# C1 VioStrategy Protocol + Composition-Root Selection

**Task**: AZ-331_c1_vio_strategy_protocol
**Name**: C1 VioStrategy Protocol
**Description**: Define the `VioStrategy` Protocol, its DTOs (`WarmStartPose`, `VioOutput`, `VioHealth`, `FeatureQuality`, `VioState` enum), the runtime error taxonomy (`VioError` family), and the composition-root selection switch that wires exactly one of `Okvis2Strategy` / `VinsMonoStrategy` / `KltRansacStrategy` at startup based on ADR-001 (config) and ADR-002 (`BUILD_OKVIS2` / `BUILD_VINS_MONO` / `BUILD_KLT_RANSAC` flags). This is the foundational shared-API task for E-C1 — every other E-C1 strategy task implements this Protocol; C5 state estimator tasks (AZ-260) consume `VioOutput`; C13 FDR writer tasks (AZ-248) consume `VioHealth`; the warm-start + F8 reboot recovery wiring task (this same epic) invokes `reset_to_warm_start`.
**Complexity**: 3 points
**Dependencies**: AZ-263_initial_structure, AZ-269_config_loader, AZ-266_log_module, AZ-270_compose_root, AZ-272_fdr_record_schema, AZ-276_imu_preintegrator, AZ-277_se3_utils
**Component**: c1_vio (epic AZ-254 / E-C1)
**Tracker**: AZ-331
**Epic**: AZ-254 (E-C1)

### Document Dependencies

- `_docs/02_document/contracts/c1_vio/vio_strategy_protocol.md` — frozen public interface this task produces.
- `_docs/02_document/contracts/shared_helpers/imu_preintegrator.md` — referenced for the IMU substrate every strategy consumes (AZ-276).
- `_docs/02_document/contracts/shared_helpers/se3_utils.md` — `SE3` math primitives used by `WarmStartPose` and `VioOutput.relative_pose_T` (AZ-277).
- `_docs/02_document/contracts/shared_config/composition_root_protocol.md` — `Config` extension for the new `config.vio.strategy` enum.
- `_docs/02_document/components/01_c1_vio/description.md` — § 1 high-level overview and § 2 internal interfaces (the Protocol's source of truth).

## Problem

Three different concrete VIO backends (OKVIS2 production-default, VINS-Mono research-only, KLT/RANSAC mandatory simple-baseline per ADR-002 engine rule) plus three external consumers (C5 state estimator, C13 FDR writer, runtime_root composition) all need a single, frozen interface to per-frame VIO output. Without it:

- Each consumer would import a concrete OKVIS2 / VINS-Mono / KLT-RANSAC class directly, hard-coding the runtime choice and breaking ADR-001's runtime selectability.
- `BUILD_OKVIS2=OFF` (Tier-0 workstation without OKVIS2 native libs installed) would not import because consumers depend on OKVIS2-specific symbols.
- The composition root would have to know per-component which strategy is acceptable; today only ADR-001 (config) + ADR-002 (`BUILD_*` flags) decide.
- Error handling would diverge per backend; `VioFatalError` would have a different shape per implementation, making the AC-5.2 fallback path fragile (3 s no-estimate timer expects a uniform error).
- C5 fusion would have to handle a different `VioOutput` shape per backend; the iSAM2 graph's `BetweenFactorPose3` insertion path assumes a fixed 6×6 covariance shape.
- The honest-covariance contract — strategies MUST NOT tighten covariance during a degradation event — would have no canonical surface to enforce; AC-1.4 drift becomes silent.

This task delivers the typed boundary every consumer reads against and every strategy conforms to. It writes no per-frame VIO logic — the concrete backends are separate tasks in this epic.

## Outcome

- A `VioStrategy` Protocol (PEP 544 `typing.Protocol`, `runtime_checkable=True`) is exported from `src/gps_denied_onboard/components/c1_vio/interface.py` and re-exported from the component's `__init__.py`.
- The DTOs `WarmStartPose`, `VioOutput`, `VioHealth`, `FeatureQuality` are stdlib `@dataclass(frozen=True)`; `VioState` is a `str`-Enum. `VioOutput` and `VioHealth` are placed in `src/gps_denied_onboard/_types/nav.py` for cross-component access (C5, C13); `WarmStartPose`, `FeatureQuality`, `VioState`, and the `VioStrategy` Protocol live in the c1_vio component.
- The runtime error taxonomy is a single hierarchy under `gps_denied_onboard.components.c1_vio.errors`: `VioError` ← {`VioInitializingError`, `VioDegradedError`, `VioFatalError`}. Every strategy raises only these; consumers catch only these.
- The composition root has a `build_vio_strategy(config: Config, *, fdr_client: FdrClient) -> VioStrategy` factory function at `src/gps_denied_onboard/runtime_root/vio_factory.py` that selects the strategy by `config.vio.strategy` (`okvis2 | vins_mono | klt_ransac`) and respects compile-time `BUILD_*` gating: requesting a strategy whose `BUILD_*` flag is OFF raises `StrategyNotAvailableError` at composition time (NOT at first frame).
- Concrete strategy modules are lazy-imported inside the factory under `if BUILD_OKVIS2: from c1_vio.okvis2 import Okvis2Strategy` so a Tier-0 workstation build that lacks OKVIS2 native libs still composes successfully when only KLT/RANSAC is requested.
- Every strategy's `current_strategy_label()` returns the lowercase label matching the config value (`"okvis2"`, `"vins_mono"`, `"klt_ransac"`); this is the FDR-stamped label for AC-NEW-3 audit.
- A `ConfigSchemaError` extension to AZ-269's config loader for the new `config.vio.strategy` enum + `config.vio.lost_frame_threshold` (default 9; consecutive-LOST frames before `VioFatalError` is raised) + `config.vio.warm_start_max_frames` (default 5; convergence budget after `reset_to_warm_start`).
- A frozen contract file at `_docs/02_document/contracts/c1_vio/vio_strategy_protocol.md` (already drafted alongside this task) carries the full shape; consumers read that file, not this task spec.

## Scope

### Included

- `VioStrategy` Protocol with the four methods from `_docs/02_document/components/01_c1_vio/description.md` § 2: `process_frame`, `reset_to_warm_start`, `health_snapshot`, `current_strategy_label`.
- DTO dataclasses for `WarmStartPose`, `VioOutput`, `VioHealth`, `FeatureQuality`. All `frozen=True`. The `VioState` enum.
- Error hierarchy under `c1_vio.errors`: every error type the Protocol promises; all derived from a common `VioError` so consumers can catch the family.
- Placement of `VioOutput` and `VioHealth` in `src/gps_denied_onboard/_types/nav.py` (per `module-layout.md` cross-component DTO rule); the rest of the surface lives in `components/c1_vio/`.
- `build_vio_strategy(config, *, fdr_client) -> VioStrategy` composition-root factory in `src/gps_denied_onboard/runtime_root/vio_factory.py`. Imports the concrete strategy lazily — guarded by `if BUILD_OKVIS2: from c1_vio.okvis2 import Okvis2Strategy` so an OFF flag does not force a native-lib import.
- A `StrategyNotAvailableError` raised by the factory when the requested strategy is not built into this binary, with a message naming the missing `BUILD_*` flag.
- Config schema extension to AZ-269's loader for the three new fields (`config.vio.strategy`, `config.vio.lost_frame_threshold`, `config.vio.warm_start_max_frames`) with enum validation at load time.
- The contract file at `_docs/02_document/contracts/c1_vio/vio_strategy_protocol.md` filled per `decompose/templates/api-contract.md` with Shape, Invariants, Non-Goals, Versioning Rules, and at least three Test Cases (already drafted as part of this task).
- Type-only unit tests that verify each strategy module's class actually conforms to the Protocol via `runtime_checkable` + `isinstance` (catches drift at CI time, not deployment).

### Excluded

- `Okvis2Strategy` implementation — separate task in this epic.
- `VinsMonoStrategy` implementation — separate task in this epic.
- `KltRansacStrategy` implementation — separate task in this epic.
- Warm-start hint persistence (write to disk after takeoff, read after F8 reboot) — separate task in this epic; this task only defines the in-memory `WarmStartPose` DTO and the `reset_to_warm_start` Protocol method.
- IMU preintegration — owned by AZ-276 (`helpers.imu_preintegrator`); strategies feed `ImuWindow` to the helper, they do not implement preintegration here.
- C5 fusion wiring — owned by E-C5 (AZ-260).
- C13 FDR consumer wiring of `VioHealth` — owned by E-C13 (AZ-248).

## Acceptance Criteria

**AC-1: Protocol is conformance-checkable**
Given a class that implements all four Protocol methods with matching signatures
When `isinstance(impl, VioStrategy)` is evaluated under `runtime_checkable`
Then the result is `True`; for a class that omits any method or has a wrong signature, the result is `False`

**AC-2: Frozen DTOs reject mutation**
Given a constructed `VioOutput(...)`, `VioHealth(...)`, `WarmStartPose(...)`, or `FeatureQuality(...)` instance
When the test attempts to reassign any field
Then `dataclasses.FrozenInstanceError` is raised; the original value is preserved

**AC-3: Error hierarchy catchable as a single family**
Given any of the three documented error subtypes
When the consumer wraps a strategy call in `try: ... except c1_vio.errors.VioError`
Then every documented subtype is caught; an unrelated `Exception` (e.g., `ValueError`) is NOT caught (the Protocol's error envelope does not leak into general exception handling)

**AC-4: Composition-root factory honours config**
Given `config.vio.strategy = "okvis2"` and `BUILD_OKVIS2=ON`
When `build_vio_strategy(config, fdr_client=fake_client)` is called
Then an `Okvis2Strategy` instance is returned and `instance.current_strategy_label() == "okvis2"`

**AC-5: Composition-root factory honours BUILD flag gate**
Given `config.vio.strategy = "vins_mono"` and `BUILD_VINS_MONO=OFF`
When `build_vio_strategy(config, fdr_client=fake_client)` is called
Then `StrategyNotAvailableError` is raised at composition time with a message naming `"vins_mono"` AND the missing `BUILD_VINS_MONO` flag; no module-level import of `vins_mono` symbols has occurred (verifiable via `sys.modules` having no `gps_denied_onboard.components.c1_vio.vins_mono` entry)

**AC-6: Unknown strategy label rejected at config load**
Given `config.vio.strategy = "openvslam"` (not in the enum)
When the config is loaded via AZ-269's loader
Then `ConfigSchemaError` is raised at load time with a message listing the valid values (`okvis2 | vins_mono | klt_ransac`); `build_vio_strategy` is never reached

**AC-7: `current_strategy_label()` matches config value exactly**
Given any selectable strategy
When `instance.current_strategy_label()` is called
Then the returned string is one of `"okvis2"`, `"vins_mono"`, `"klt_ransac"` and equals `config.vio.strategy`; AC-NEW-3 audit relies on this exact-match property

**AC-8: Contract file matches Protocol shape**
Given the contract file at `_docs/02_document/contracts/c1_vio/vio_strategy_protocol.md`
When a contract-test parses the Shape section's method/field tables and compares against the runtime Protocol via introspection
Then every method, every field, every error type is present and consistent in both

**AC-9: `VioOutput.frame_id` echo invariant typed**
Given the DTO definitions
When inspecting `VioOutput.frame_id`'s type and the Protocol's docstring
Then `frame_id` is typed `str` and the docstring states "MUST equal `NavCameraFrame.frame_id` from the input frame"; AC-9 is enforced behaviourally by the strategy implementations and contract-tested in Step 9

## Non-Functional Requirements

**Compatibility**
- The Protocol is `typing.Protocol` (PEP 544 structural typing) so existing components that import a concrete VIO class today (none yet — this is greenfield) can be retrofitted without inheritance changes.
- All error types subclass `Exception` (not `BaseException`) so `except Exception:` in upstream layers continues to work as expected.

**Performance**
- The factory `build_vio_strategy` returns within 200 ms (it imports + constructs one strategy; native-library bring-up time inside OKVIS2 / VINS-Mono is amortised against this; the 200 ms is a sanity bound for the factory dispatch logic itself, not for OKVIS2 backend init).
- DTO construction (`VioOutput`, `VioHealth`, `WarmStartPose`) is dataclass-frozen; per-instance overhead is the bare-cost dataclass `__init__`.

**Reliability**
- The Protocol is the boundary of acceptable runtime errors. Strategies MUST NOT raise other types into consumers; if a third-party library (OpenCV, OKVIS2, VINS-Mono, GTSAM) raises something else, the strategy catches and rewraps into `VioError` family.
- Versioning: any breaking change to the Protocol or its DTOs MUST bump the contract file's `Version` and notify every consumer task listed in the contract header.

## Unit Tests

| AC Ref | What to Test | Required Outcome |
|--------|-------------|-----------------|
| AC-1 | `runtime_checkable` Protocol vs. a fully-implementing fake; vs. a fake missing one method | `isinstance` returns True for full, False for partial |
| AC-2 | Mutation attempt on each frozen DTO | `FrozenInstanceError` raised; original value preserved |
| AC-3 | Raise each of the three error subtypes; catch as `c1_vio.errors.VioError` | All caught; an unrelated `ValueError` is NOT caught by the same handler |
| AC-4 | `build_vio_strategy` with `okvis2` + flag ON → fake `Okvis2Strategy` | Returned instance is `Okvis2Strategy`; `current_strategy_label()` == `"okvis2"` |
| AC-5 | `build_vio_strategy` with `vins_mono` + flag OFF | `StrategyNotAvailableError`; `sys.modules` does NOT contain `c1_vio.vins_mono` |
| AC-6 | Config load with invalid `vio.strategy` value | `ConfigSchemaError`; valid values listed in message |
| AC-7 | `current_strategy_label()` for each strategy | Matches the config value used to construct it |
| AC-8 | Contract introspection vs. Protocol introspection | Shape parity test passes |
| AC-9 | Inspect `VioOutput.frame_id` type and docstring | `str` type; echo-invariant noted in docstring |
| NFR-perf-factory | Microbench `build_vio_strategy` × 1000 (with each strategy mocked) | p99 ≤ 200 ms (dominated by lazy import on first call; subsequent calls << 1 ms) |
| NFR-reliability-error-family | All three subtypes inherit from `c1_vio.errors.VioError` | Verified via `issubclass` for each |

## Constraints

- The Protocol uses `typing.Protocol` from stdlib; no third-party Protocol library is introduced.
- DTO dataclasses use stdlib `dataclasses` with `frozen=True`; no `pydantic` or `attrs` dependency.
- Lazy import of concrete strategies is mandatory. The factory's `if BUILD_OKVIS2: from c1_vio.okvis2 import Okvis2Strategy` block is not optional — it is the mechanism by which Tier-0 workstation builds compose without OKVIS2 native libs installed.
- The contract file at `_docs/02_document/contracts/c1_vio/vio_strategy_protocol.md` is the source of truth. If the Protocol shape changes here without the contract updating, that is a Spec-Gap finding (High) per code-review skill Phase 2.
- This task does NOT add new third-party dependencies — `typing.Protocol`, `dataclasses`, `enum` are stdlib.
- `VioOutput` and `VioHealth` MUST be placed in `src/gps_denied_onboard/_types/nav.py` per `module-layout.md` (cross-component DTOs live under `_types`); placing them inside `components/c1_vio/` would force C5 and C13 to import a Layer-3 module from another Layer-3 module, violating the layering table.

## Risks & Mitigation

**Risk 1: Protocol drift between contract and code**
- *Risk*: Strategy implementations diverge from the contract over time; consumers cannot tell which is canonical.
- *Mitigation*: AC-8 contract-introspection test runs in CI; any drift fails the test before merge. The contract file's `## Test Cases` section names this exact test.

**Risk 2: Lazy-import gating is bypassed by a transitively-imported module**
- *Risk*: A consumer imports `c1_vio` (the package) and the package's `__init__.py` eagerly imports a concrete strategy, triggering the OKVIS2 native-lib import even when `BUILD_OKVIS2=OFF`.
- *Mitigation*: The package `__init__.py` re-exports ONLY the Protocol and DTOs and errors — it does NOT import any concrete strategy. AC-5 verifies via `sys.modules` that no strategy module is loaded during a Tier-0 factory call.

**Risk 3: `VioDegradedError` is misused as a hard error**
- *Risk*: A strategy implementation interprets degraded operation as a raise condition and stops emitting `VioOutput`; C5 fusion thinks VIO is dead and falls back to FC IMU prematurely, missing AC-1.3 drift bound during recoverable degradations.
- *Mitigation*: Contract file Invariants section explicitly states degraded operation returns `VioOutput` with inflated covariance + `VioHealth.state = DEGRADED`; the error type exists only for the rare degraded-to-fatal transition. Strategy task specs (separate tasks in this epic) cite this in their Constraints.

**Risk 4: Error hierarchy widens silently**
- *Risk*: A future strategy adds a fourth error type without updating the contract or the family base class.
- *Mitigation*: The contract file lists the canonical three. Implementations MUST raise only members of `c1_vio.errors.VioError`; a strategy raising a non-family error is a Spec-Gap finding (High) at code-review time. AC-3's catch-as-family test catches the obvious case.

## Runtime Completeness

- **Named capability**: typed Protocol + DTOs + error envelope + composition-root selection (architecture / E-C1 / ADR-001 + ADR-002 + ADR-009).
- **Production code that must exist**: real Protocol declaration, real frozen DTOs, real error hierarchy, real composition-root factory with lazy-import gating, real config-loader extension for the strategy enum + thresholds.
- **Allowed external stubs**: tests MAY substitute fake strategy classes that conform to the Protocol; production wiring uses the real strategies from this epic's other tasks.
- **Unacceptable substitutes**: ABCs instead of `typing.Protocol` (would force inheritance changes downstream), `pydantic.BaseModel` instead of `@dataclass(frozen=True)` (would add a runtime validation layer this task does not need), eager imports of concrete strategies in `__init__.py` (would defeat `BUILD_*` gating), or a `strategy: str` config field without an enum (would lose the load-time validation in AC-6).

## Contract

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