[AZ-589] [AZ-590] Close completeness gate cycle 1: VIO remediation tasks

The Product Implementation Completeness Gate (cycle 1, 2026-05-16)
audited 107 done product tasks. 105 PASS / 0 BLOCKED / 2 FAIL.

FAIL findings — both AZ-332 (OKVIS2) and AZ-333 (VINS-Mono) ship a
real Python facade + AC-tested fake backend, but their native pybind11
bindings (_native/okvis2_binding.cpp, _native/vins_mono_binding.cpp)
are skeletons: _build_estimator() sets estimator_built_ = false; the
first add_frame() raises *FatalException("estimator not yet wired").
Production-default VIO and the comparative-study path both crash on
the first nav-camera frame.

Remediation tasks created in _docs/02_tasks/todo/:
  - AZ-589  remediate_okvis2_threadedkfvio_wiring  (5pt)
  - AZ-590  remediate_vins_mono_estimator_wiring   (5pt)

Both tasks also seed the per-binary bootstrap register_strategy() call
sites — the existing strategy registry in runtime_root/__init__.py is
never invoked in src/ today.

Artifacts:
  - _docs/03_implementation/implementation_completeness_cycle1_report.md
  - _docs/02_tasks/todo/AZ-589_remediate_okvis2_threadedkfvio_wiring.md
  - _docs/02_tasks/todo/AZ-590_remediate_vins_mono_estimator_wiring.md
  - _docs/02_tasks/_dependencies_table.md  (+2 rows; totals refreshed)
  - _docs/_autodev_state.md                (Step 7 phase 1 parse;
                                            current_batch: 66)

Returning to implement-skill Step 1 to parse Batch 66 against these
remediation tasks (per Step 15 option A).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-16 10:24:38 +03:00
parent c5ffc14fe9
commit be5c6d20aa
5 changed files with 597 additions and 8 deletions
+5 -3
View File
@@ -1,8 +1,8 @@
# Dependencies Table
**Date**: 2026-05-16 (refreshed at end of Batch 64: AZ-558 implementation closed — `MavlinkTransport` seam now routes every C8 outbound MAVLink byte; AZ-401 AC-9 + AZ-404 AC-4b unskipped together; encoder helpers extracted to `_outbound_mavlink_payloads.py`; live-mode `compose_root` injection deferred to whichever future batch registers AP/iNav strategies in an airborne binary; earlier 2026-05-14: refreshed at start of Batch 63: AZ-559 closed Won't Fix — gap was illusory; `TileSource.ONBOARD_INGEST` + `TileMetadata.quality_metadata` + `write_tile`'s `FreshnessRejectionError` already cover the AZ-389 mid-flight ingest semantic without any new API; AZ-389 dep restored to AZ-303; earlier same-day after Batch 61: AZ-558 follow-up added — routes C8 outbound encoder bytes through `MavlinkTransport` seam; closes AZ-401 AC-9 deferred during batch 61 due to encoder-side routing not being in the AZ-401 task envelope; earlier same-day after cumulative review batches 52-54: AZ-528 hygiene PBI added for c1_vio strategy facade orchestration-spine 3-way duplication (Medium); earlier same-day after Batch 53: AZ-333 VINS-Mono landed — first c1_vio strategy after the AZ-332 OKVIS2 production-default; consolidation hygiene for the strategy-facade duplication deferred to a post-AZ-334 PBI; earlier same-day after Batch 51: AZ-527 hygiene PBI added from cumulative review batches 49-51 F1; 2026-05-13: AZ-526 hygiene PBI added from cumulative review batches 46-48 F1+F3; same-day refresh after Batch 44 SRP refactor: AZ-317 superseded; AZ-329 + AZ-330 specs rewritten; AZ-523 + AZ-524 audit-trail tickets added; E-C12 epic renamed `Operator Pre-flight Tooling``Operator Pre-flight Orchestrator`; earlier same-day refresh: AZ-507 + AZ-508 hygiene PBIs from cumulative review batches 31-33; 2026-05-11: AZ-489 + AZ-490 ADR-010 operator-origin path)
**Total Tasks**: 150 (109 product + 41 blackbox-test) — AZ-317 retained in the table marked SUPERSEDED for audit; AZ-523 (C11 gate removal) + AZ-524 (C12 rename) added as 2 closed audit-trail tasks; AZ-526 = 2pt clock-helper hygiene; AZ-527 = 2pt c2 engine-dim helper hygiene; AZ-528 = 3pt c1_vio facade-spine hygiene; AZ-558 = 3pt MavlinkTransport routing follow-up; AZ-559 closed Won't Fix
**Total Complexity Points**: 497 (364 product + 133 blackbox-test) — AZ-523 = 3pt, AZ-524 = 2pt, AZ-526 = 2pt, AZ-527 = 2pt, AZ-528 = 3pt, AZ-558 = 3pt
**Date**: 2026-05-16 (refreshed at end of Product Implementation Completeness Gate cycle 1: 2 FAIL items (AZ-332, AZ-333) → AZ-589 + AZ-590 remediation tasks added; spec files `_docs/02_tasks/todo/AZ-58[9-90]_remediate_*.md`; gate report `_docs/03_implementation/implementation_completeness_cycle1_report.md`; earlier same-day after end of Batch 64: AZ-558 implementation closed — `MavlinkTransport` seam now routes every C8 outbound MAVLink byte; AZ-401 AC-9 + AZ-404 AC-4b unskipped together; encoder helpers extracted to `_outbound_mavlink_payloads.py`; live-mode `compose_root` injection deferred to whichever future batch registers AP/iNav strategies in an airborne binary; earlier 2026-05-14: refreshed at start of Batch 63: AZ-559 closed Won't Fix — gap was illusory; `TileSource.ONBOARD_INGEST` + `TileMetadata.quality_metadata` + `write_tile`'s `FreshnessRejectionError` already cover the AZ-389 mid-flight ingest semantic without any new API; AZ-389 dep restored to AZ-303; earlier same-day after Batch 61: AZ-558 follow-up added — routes C8 outbound encoder bytes through `MavlinkTransport` seam; closes AZ-401 AC-9 deferred during batch 61 due to encoder-side routing not being in the AZ-401 task envelope; earlier same-day after cumulative review batches 52-54: AZ-528 hygiene PBI added for c1_vio strategy facade orchestration-spine 3-way duplication (Medium); earlier same-day after Batch 53: AZ-333 VINS-Mono landed — first c1_vio strategy after the AZ-332 OKVIS2 production-default; consolidation hygiene for the strategy-facade duplication deferred to a post-AZ-334 PBI; earlier same-day after Batch 51: AZ-527 hygiene PBI added from cumulative review batches 49-51 F1; 2026-05-13: AZ-526 hygiene PBI added from cumulative review batches 46-48 F1+F3; same-day refresh after Batch 44 SRP refactor: AZ-317 superseded; AZ-329 + AZ-330 specs rewritten; AZ-523 + AZ-524 audit-trail tickets added; E-C12 epic renamed `Operator Pre-flight Tooling``Operator Pre-flight Orchestrator`; earlier same-day refresh: AZ-507 + AZ-508 hygiene PBIs from cumulative review batches 31-33; 2026-05-11: AZ-489 + AZ-490 ADR-010 operator-origin path)
**Total Tasks**: 152 (111 product + 41 blackbox-test) — AZ-317 retained in the table marked SUPERSEDED for audit; AZ-523 (C11 gate removal) + AZ-524 (C12 rename) added as 2 closed audit-trail tasks; AZ-526 = 2pt clock-helper hygiene; AZ-527 = 2pt c2 engine-dim helper hygiene; AZ-528 = 3pt c1_vio facade-spine hygiene; AZ-558 = 3pt MavlinkTransport routing follow-up; AZ-559 closed Won't Fix; AZ-589 = 5pt OKVIS2 ThreadedKFVio remediation; AZ-590 = 5pt VINS-Mono estimator remediation
**Total Complexity Points**: 507 (374 product + 133 blackbox-test) — AZ-523 = 3pt, AZ-524 = 2pt, AZ-526 = 2pt, AZ-527 = 2pt, AZ-528 = 3pt, AZ-558 = 3pt, AZ-589 = 5pt, AZ-590 = 5pt
Dependencies columns list only the tracker-ID portion (descriptive tail
text in each task spec is omitted here for table-readability). The
@@ -164,6 +164,8 @@ are all declared and documented below under **Cycle Check**.
| AZ-528 | Hygiene — consolidate c1_vio strategy facade orchestration spine | 3 | AZ-334 | AZ-254 |
| AZ-523 | Batch 44 — C11 internal flight-state gate removal (SRP refactor; audit-trail; closed) | 3 | AZ-317, AZ-319, AZ-329 | AZ-251 |
| AZ-524 | Batch 44 — C12 package rename: c12_operator_tooling → c12_operator_orchestrator (audit; closed)| 2 | AZ-263, AZ-326, AZ-327, AZ-328, AZ-329, AZ-330, AZ-489 | AZ-253 |
| AZ-589 | Remediate AZ-332 — wire `okvis::ThreadedKFVio` inside OKVIS2 pybind11 binding | 5 | AZ-332, AZ-276, AZ-277 | AZ-254 |
| AZ-590 | Remediate AZ-333 — wire VINS-Mono estimator inside pybind11 binding | 5 | AZ-333, AZ-276, AZ-277 | AZ-254 |
## Notes
@@ -0,0 +1,106 @@
# Remediate AZ-332 — wire `okvis::ThreadedKFVio` inside the OKVIS2 pybind11 binding
**Task**: AZ-589_remediate_okvis2_threadedkfvio_wiring
**Name**: AZ-332 ThreadedKFVio wiring (production-default VIO)
**Description**: Replace the AZ-332 skeleton `_native/okvis2_binding.cpp` `_build_estimator()` / `_drive_estimator()` paths with the real `okvis::ThreadedKFVio` estimator wiring. Without this, the airborne deployment binary cannot process a single nav-camera frame (`Okvis2Backend.add_frame` throws `OkvisFatalException("OKVIS2 estimator not yet wired — this binding is the AZ-332 skeleton")` on the first call).
**Complexity**: 5 points
**Dependencies**: AZ-332, AZ-276 (ImuPreintegrator), AZ-277 (SE3Utils)
**Component**: c1_vio (epic AZ-254 / E-C1)
**Tracker**: AZ-589
**Epic**: AZ-254 (E-C1)
## Problem
The Product Implementation Completeness Gate (cycle 1, 2026-05-16) classified AZ-332 as **FAIL**:
- `src/gps_denied_onboard/components/c1_vio/_native/okvis2_binding.cpp` lines 251272: `_build_estimator()` always sets `estimator_built_ = false`; `_drive_estimator()` throws `OkvisFatalException("Okvis2Backend: OKVIS2 estimator not yet wired ...")`.
- The OKVIS2 upstream headers are still commented out (line 48 `// #include <okvis/ThreadedKFVio.hpp>`).
- AZ-332's `Runtime Completeness` section forbids "a pre-built deterministic-fallback `VioOutput` while OKVIS2 is 'compiled out'"; the current binding violates that contract.
AZ-332's own `Implementation Notes (2026-05-12, batch 23)` flagged this for the gate and named the follow-up `AZ-332_tier2_validation`. This task discharges that contract.
Production-default VIO must operate before F3 (Steady-state per-frame estimation) can run end-to-end on the airborne binary.
## Outcome
- `_native/okvis2_binding.cpp::_build_estimator()` instantiates `okvis::ThreadedKFVio` from `yaml_config_`. The real OKVIS2 upstream headers (`okvis/ThreadedKFVio.hpp`, `okvis/Estimator.hpp`, `okvis/VioParametersReader.hpp`) are uncommented and linked.
- Output callback attached to `ThreadedKFVio` fills `latest_output_` (under `output_mtx_`) with the real `EstimatorOutput`: SE(3) pose, marginalised 6×6 covariance, accel/gyro biases, feature counts, mean parallax, per-frame MRE.
- `_drive_estimator(image)` forwards the frame to the real estimator, returns `true` on a new keyframe output and `false` otherwise. NO `throw` after first frame on the happy path.
- `add_imu(ts_ns, accel, gyro)` pushes IMU into `ThreadedKFVio` via the real upstream API (no longer just caching `last_accel_` / `last_gyro_`).
- `reset(...)` reinitialises the estimator from the seed pose / velocity / biases via the upstream reset surface; bias propagation goes through the AZ-276 substrate where the Python facade owns it.
- CMake glue at `cpp/okvis2/CMakeLists.txt` is upgraded so `BUILD_OKVIS2=ON` actually links the upstream library (Ceres, Brisk, OpenGV, OKVIS2 itself); the Linux Tier-1 CI workflow comment `-DBUILD_OKVIS2=OFF` can be flipped to `ON` once the build succeeds in the GitHub Actions runner image.
- The existing AC-1..AC-8 + AC-10 unit-test suite (`tests/unit/c1_vio/test_okvis2_strategy.py` + `conftest.py`'s `FakeOkvis2Backend`) continues to pass — the Python facade contract is unchanged.
- A new Tier-1-runnable integration test exercises the real binding against a small fixture (`tests/integration/c1_vio/test_okvis2_real_binding.py`, marked `@pytest.mark.tier1_real_okvis2 + @pytest.mark.skipif(not _okvis2_binding_present())` so Tier-1 CI without OKVIS2 still passes).
- The `runtime_root` per-binary bootstrap module registers the `okvis2` strategy via `register_strategy("c1_vio", "okvis2", ...)` so the deployment binary can actually resolve it (currently the registry is empty — see § Notes below).
## Scope
### Included
- `_native/okvis2_binding.cpp` wiring of `okvis::ThreadedKFVio` (`_build_estimator`, `_drive_estimator`, `add_imu`, `reset`, output callback).
- `cpp/okvis2/CMakeLists.txt` upstream link + transitive Ceres/Brisk/OpenGV dependency declaration.
- One Tier-1-runnable integration test against a tiny fixture.
- A short `_docs/04_refactoring/runtime_bootstrap_register_okvis2/` note describing the per-binary bootstrap registration call site (the registry seam already exists in `runtime_root/__init__.py`; this task adds the actual `register_strategy(...)` call inside the deployment binary's bootstrap).
### Excluded
- AC-9 honest-covariance Tier-2 validation against Derkachi-class fixtures on real Jetson hardware — separate Tier-2 perf task (`tier2_AZ-332_honest_covariance_validation`).
- Comparative-study (IT-12) test suite changes — owned by `AZ-444_tier2_jetson_harness` and downstream test tasks.
- OKVIS2 upstream-source modifications — pinned per Plan-phase; deviations require a separate ADR.
- VINS-Mono wiring — separate remediation task (`remediate_vins_mono_estimator_wiring`).
## Acceptance Criteria
**AC-1: `_drive_estimator` returns without raising on a valid nav-camera frame**
Given a real `Okvis2Backend` constructed with the test fixture's YAML + intrinsics
When `add_frame("uuid-abc", ts_ns=…, image=…)` is called on a well-formed image
Then the call returns a `bool` (true on keyframe output, false otherwise); no `OkvisFatalException` is raised on the "estimator not yet wired" path
**AC-2: Output callback populates `latest_output_` after the first keyframe**
Given a sequence of N frames feeding a normal-segment fixture
When `get_latest_output()` is called after a keyframe is emitted
Then the returned dict contains `pose_T_world_body` (4×4 finite float64), `pose_covariance_6x6` (6×6 SPD float64), `accel_bias` + `gyro_bias` (length-3 finite float64), and integer feature counts — all values reflect the real estimator state, not seed zeros
**AC-3: Python facade unit tests stay green**
Given the existing `Okvis2Strategy` AC-1..AC-8 + AC-10 unit tests (which use `FakeOkvis2Backend`)
When `pytest tests/unit/c1_vio/test_okvis2_strategy.py` runs
Then 100% pass (the facade contract is preserved)
**AC-4: `BUILD_OKVIS2=OFF` still produces an importable package**
Given a build with `BUILD_OKVIS2=OFF`
When `import gps_denied_onboard.components.c1_vio` runs
Then the package imports without ever touching `_native.okvis2_binding`; `gps_denied_onboard.components.c1_vio.okvis2` is not auto-imported; the AZ-331 factory raises `StrategyNotAvailableError("okvis2", missing_flag="BUILD_OKVIS2")` if `okvis2` is requested
**AC-5: Composition-root strategy registration is live**
Given the deployment binary's per-binary bootstrap module
When `compose_root(config)` runs with `config.components["c1_vio"].strategy == "okvis2"`
Then `register_strategy("c1_vio", "okvis2", build_okvis2_strategy, tier="airborne", depends_on=(…))` has been called at module import time; `_resolve_strategy` returns the registration without raising `StrategyNotLinkedError`
## Non-Functional Requirements
**Performance**
- `_drive_estimator` first-call construction cost ≤ 5 s (one-time, swallowed by AC-NEW-1 boot budget).
- Steady-state `_drive_estimator` per-frame cost ≤ 80 ms p95 on Tier-2 (AC-9 / NFR-perf validation is the separate Tier-2 task; this task only verifies the wiring is not introducing an obviously broken path).
**Reliability**
- No raw OKVIS2 / Ceres / Eigen exceptions cross the pybind11 boundary; everything is caught and rewrapped into `OkvisInitException` / `OkvisOptimizationException` / `OkvisFatalException`.
## Constraints
- Per `ADR-002`, the `BUILD_OKVIS2=OFF` deployment binary must still build and pass all non-OKVIS2 tests after this change.
- OKVIS2 upstream is pinned per Plan-phase; the build must use the pinned commit, not HEAD.
- AZ-276 `ImuPreintegrator` remains a *separate* substrate for E-C5's fusion graph; OKVIS2's internal IMU integration is owned by `ThreadedKFVio`. The single-IMU-truth invariant operates at the IMU sample-stream level, NOT at the integrator-instance level (resolved in AZ-332 § Implementation Notes).
## Risks & Mitigation
**Risk 1: OKVIS2 upstream build fails on the GitHub Actions Linux runner image**
- *Risk*: Ceres / Brisk / OpenGV require specific system packages (Eigen ≥ 3.4, gflags, glog, suitesparse, …). The default `ubuntu-latest` image may be missing dependencies.
- *Mitigation*: lift the `apt install` block from `cpp/okvis2/README` (or upstream's `INSTALL.md`) into `.github/workflows/ci.yml`'s "Setup C++ deps" step before invoking CMake. Document the dependency list in the task report.
**Risk 2: ABI conflict between OKVIS2's Ceres pin and the project's GTSAM pin**
- *Risk*: GTSAM uses its own internal Ceres-less optimisation; OKVIS2 needs Ceres. If both libraries pull in conflicting Eigen versions, the airborne binary will segfault at link time.
- *Mitigation*: vendor OKVIS2's third-party dependencies under `cpp/_third_party/okvis2/` with strict version pinning; verify `objdump -T` or equivalent that the Eigen symbol set matches between GTSAM-compiled and OKVIS2-compiled objects.
## Notes
This remediation also incidentally exposes a cross-cutting gap noted in the cycle-1 completeness report: `runtime_root/__init__.py` defines a strategy registry but no module currently calls `register_strategy(...)` — meaning even with this wiring complete, `compose_root` would still raise `StrategyNotLinkedError` on the deployment binary until a per-binary bootstrap module is added. AC-5 above ensures this task lands at least the `c1_vio` slot's registration; the broader cross-component registration sweep is tracked as a separate cross-cutting task (proposed `cross_cutting_per_binary_bootstrap`).
@@ -0,0 +1,104 @@
# Remediate AZ-333 — wire VINS-Mono estimator inside the pybind11 binding
**Task**: AZ-590_remediate_vins_mono_estimator_wiring
**Name**: AZ-333 VINS-Mono estimator wiring (research / comparative VIO)
**Description**: Replace the AZ-333 skeleton `_native/vins_mono_binding.cpp` `_build_estimator()` / `_drive_estimator()` paths with the real VINS-Mono estimator wiring. Without this, the `VinsMonoStrategy` cannot produce a single pose update at runtime — the binding throws `VinsMonoFatalException("VINS-Mono estimator not yet wired ... AZ-333 skeleton")` on the first frame.
**Complexity**: 5 points
**Dependencies**: AZ-333, AZ-276 (ImuPreintegrator), AZ-277 (SE3Utils)
**Component**: c1_vio (epic AZ-254 / E-C1)
**Tracker**: AZ-590
**Epic**: AZ-254 (E-C1)
## Problem
The Product Implementation Completeness Gate (cycle 1, 2026-05-16) classified AZ-333 as **FAIL**:
- `src/gps_denied_onboard/components/c1_vio/_native/vins_mono_binding.cpp` exhibits the identical skeleton pattern as `okvis2_binding.cpp`: `_build_estimator()` sets `estimator_built_ = false`; `_drive_estimator()` throws `VinsMonoFatalException("VinsMonoBackend: VINS-Mono estimator not yet wired — this binding is the AZ-333 skeleton")`.
- VINS-Mono upstream headers (`estimator/estimator.h`, `feature_tracker.h`) are commented out.
- AZ-333's `Runtime Completeness` section explicitly requires "a real pybind11 binding around upstream VINS-Mono" and "real per-frame estimator update" — both currently unmet.
VINS-Mono is the **research / comparative VIO strategy** (`tier=research`), used by IT-12 comparative-study runs and the Tier-2 Jetson harness. Until wired, all OKVIS2-vs-VINS-Mono comparison runs would crash on the VINS-Mono branch.
## Outcome
- `_native/vins_mono_binding.cpp::_build_estimator()` instantiates the upstream `Estimator` and `FeatureTracker`, wired with the AZ-333 YAML config and intrinsics.
- `_drive_estimator(image)` calls `feature_tracker_.readImage(image, ts)` followed by `estimator_.processMeasurements(...)`, returning `true` on a new keyframe output and `false` otherwise.
- `add_imu(ts_ns, accel, gyro)` pushes IMU into the estimator's IMU queue (no longer just caching).
- Output extraction reads the estimator's most recent `Vector<double, 7>` pose + bias state and the marginalisation block's covariance.
- `reset(...)` reinitialises the estimator via VINS-Mono's `clearState() + setParameter()` surface from a seed pose / velocity / biases.
- CMake glue at `cpp/vins_mono/CMakeLists.txt` is upgraded so `BUILD_VINS_MONO=ON` actually links upstream VINS-Mono (Ceres, OpenCV ≥ 4.2, Eigen ≥ 3.4); the Tier-2 perf workflow can flip `-DBUILD_VINS_MONO=OFF` to `ON`.
- The existing AC-1..AC-9 unit-test suite (`tests/unit/c1_vio/test_vins_mono_strategy.py` + `conftest.py`'s `FakeVinsMonoBackend`) continues to pass — the Python facade contract is unchanged.
- A new Tier-1-runnable integration test exercises the real binding against a small fixture, gated behind `@pytest.mark.tier1_real_vins_mono + @pytest.mark.skipif(not _vins_mono_binding_present())`.
- The per-binary bootstrap module's strategy registry now includes a `register_strategy("c1_vio", "vins_mono", ...)` call for the research / comparative-study binary.
## Scope
### Included
- `_native/vins_mono_binding.cpp` wiring of the real `Estimator` + `FeatureTracker` (`_build_estimator`, `_drive_estimator`, `add_imu`, `reset`, pose / covariance extraction).
- `cpp/vins_mono/CMakeLists.txt` upstream link + transitive Ceres / OpenCV / Eigen dependency declaration.
- One Tier-1-runnable integration test against a tiny fixture.
- `runtime_root` registration call site for the `vins_mono` strategy (added inside the research / comparative-study binary's bootstrap, NOT the airborne binary).
### Excluded
- AZ-444 Tier-2 Jetson comparative-study harness changes — that task owns the IT-12 run orchestration.
- VINS-Mono upstream-source modifications — pinned per Plan-phase; deviations require a separate ADR.
- OKVIS2 wiring — sibling remediation task (`remediate_okvis2_threadedkfvio_wiring`).
- Replacing VINS-Mono with VINS-Fusion or VINS-RGBD — pinned upstream variant is monocular VINS-Mono.
## Acceptance Criteria
**AC-1: `_drive_estimator` returns without raising on a valid nav-camera frame**
Given a real `VinsMonoBackend` constructed with the test fixture's YAML + intrinsics
When `add_frame("uuid-abc", ts_ns=…, image=…)` is called on a well-formed image
Then the call returns a `bool` (true on keyframe output, false otherwise); no `VinsMonoFatalException` is raised on the "estimator not yet wired" path
**AC-2: Output callback populates `latest_output_` after the first keyframe**
Given a sequence of N frames feeding a normal-segment fixture
When `get_latest_output()` is called after a keyframe is emitted
Then the returned dict contains `pose_T_world_body` (4×4 finite float64), `pose_covariance_6x6` (6×6 SPD float64), `accel_bias` + `gyro_bias` (length-3 finite float64), and integer feature counts — all values reflect the real estimator state, not seed zeros
**AC-3: Python facade unit tests stay green**
Given the existing `VinsMonoStrategy` AC-1..AC-9 unit tests (which use `FakeVinsMonoBackend`)
When `pytest tests/unit/c1_vio/test_vins_mono_strategy.py` runs
Then 100% pass (the facade contract is preserved)
**AC-4: `BUILD_VINS_MONO=OFF` still produces an importable package**
Given a build with `BUILD_VINS_MONO=OFF`
When `import gps_denied_onboard.components.c1_vio` runs
Then the package imports without ever touching `_native.vins_mono_binding`; `gps_denied_onboard.components.c1_vio.vins_mono` is not auto-imported; the AZ-331 factory raises `StrategyNotAvailableError("vins_mono", missing_flag="BUILD_VINS_MONO")` if `vins_mono` is requested
**AC-5: Research binary's strategy registration is live**
Given the comparative-study / research binary's per-binary bootstrap module
When `compose_root(config)` runs with `config.components["c1_vio"].strategy == "vins_mono"`
Then `register_strategy("c1_vio", "vins_mono", build_vins_mono_strategy, tier="research", depends_on=(…))` has been called at module import time; `_resolve_strategy` returns the registration without raising `StrategyNotLinkedError`
## Non-Functional Requirements
**Performance**
- `_drive_estimator` first-call construction cost ≤ 5 s (one-time, swallowed by the boot budget).
- Steady-state `_drive_estimator` per-frame cost ≤ 100 ms p95 on Tier-2 (NFR-perf validation lives in the Tier-2 Jetson harness; this task only verifies the wiring is not obviously broken).
**Reliability**
- No raw VINS-Mono / Ceres / Eigen exceptions cross the pybind11 boundary; everything is caught and rewrapped into `VinsMonoInitException` / `VinsMonoOptimizationException` / `VinsMonoFatalException`.
## Constraints
- Per `ADR-002`, the `BUILD_VINS_MONO=OFF` deployment binary must still build and pass all non-VINS-Mono tests after this change.
- VINS-Mono upstream is pinned per Plan-phase; build against the pinned commit, not HEAD.
- The research binary's bootstrap must not auto-load on the airborne `BUILD_OKVIS2=ON, BUILD_VINS_MONO=OFF` deployment binary — that would defeat the build-flag isolation invariant.
## Risks & Mitigation
**Risk 1: VINS-Mono's ROS-isms in its upstream source**
- *Risk*: Upstream VINS-Mono carries ROS/Catkin assumptions (rosbag input, ROS-flavoured visualisation). These need to be either stripped or stubbed; otherwise the link step pulls in ROS, which the project intentionally does not depend on.
- *Mitigation*: vendor a minimal-ROS-stub layer under `cpp/_third_party/vins_mono_ros_stub/` (already a documented pattern in the AZ-444 Plan notes); pre-process the upstream sources to compile against this stub.
**Risk 2: Eigen ABI conflict with GTSAM / OKVIS2**
- *Risk*: VINS-Mono, OKVIS2, and GTSAM all transitively depend on Eigen; if their Eigen pins disagree, ABI breakage at link time.
- *Mitigation*: enforce a single Eigen pin across `cpp/_third_party/` via the top-level `CMakeLists.txt`; verify the shared Eigen symbol set matches.
## Notes
Identical structural defect to AZ-332. Mitigations and gotchas overlap heavily, so the two remediation tasks should ideally be scheduled in the same batch (the CMake / dependency-management work shares a lot of surface area). The composition-root registry gap (`runtime_root/__init__.py::register_strategy(...)` is never called in `src/`) is also touched by both tasks; coordinate the per-binary bootstrap module so both remediations land their `register_strategy` call in the right binary (airborne for OKVIS2, research/comparative for VINS-Mono).