[AZ-623] [AZ-625] Phase E: c282_ransac + c5 helpers; split handle work

Wire 4 stateless / cached helpers into airborne_bootstrap.build_pre_constructed:
c282_ransac_filter, c5_imu_preintegrator (cached on calibration path),
c5_se3_utils (helpers.se3_utils module as namespace handle), c5_wgs_converter.

The original AZ-623 5th deliverable (c5_isam2_graph_handle) hit an
unresolvable construction-order conflict between c4_pose (consumes the handle)
and c5_state (creates it inside build_state_estimator's tuple return) under
the umbrella's "MUST NOT touch any per-component factory signature" constraint.
Per AZ-623 spec's escalation gate, scope was split: AZ-625 captures the handle
ordering work; AZ-624 dependency edge updated to require both.

Tests: tests/unit/runtime_root/test_az623_pre_constructed_phase_e.py adds 7
tests covering AC-623.1..3 (4 new keys + correct types, IMU preintegrator
caching, operator-actionable error messages for empty / unreadable / malformed
calibration paths). Autouse stubs added to test_az619/620/621/622 so prior
phase tests remain isolated from new builders.

Quality gates: ruff format clean, ruff lint clean, 24/24 phase tests pass,
247/247 runtime_root + c5_state regression suite passes. Code review verdict
PASS_WITH_WARNINGS (3 Low findings; full report in
_docs/03_implementation/reviews/batch_94_review.md).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-19 09:20:28 +03:00
parent 5c4d129f80
commit 02208c577e
13 changed files with 1014 additions and 151 deletions
@@ -0,0 +1,95 @@
# Batch Report
**Batch**: 94
**Tasks**: AZ-623 (Phase E narrowed: build_pre_constructed seeds c282_ransac_filter + c5_imu_preintegrator + c5_se3_utils + c5_wgs_converter; `c5_isam2_graph_handle` work split out to AZ-625)
**Date**: 2026-05-19
**Cycle**: 1
## Task Results
| Task | Status | Files Modified | Tests | AC Coverage | Issues |
|------|--------|----------------|-------|-------------|--------|
| AZ-623_pre_constructed_phase_e_ransac_c5_helpers (narrowed) | Done | 7 files | 24 passed | 4/4 ACs covered | 0 blocking |
Original AZ-623 had a 5th deliverable (`c5_isam2_graph_handle`) that uncovered an unresolvable construction-order conflict between `c4_pose` (consumes the handle) and `c5_state` (creates the handle inside `build_state_estimator`'s tuple return) under the umbrella's "MUST NOT touch any per-component factory signature" constraint. Per the AZ-623 spec's own escalation gate ("if the resolution requires a Protocol seam change, escalate"), the question was surfaced; user chose to **split scope**:
- This batch lands the 4 stateless / cached helpers (narrowed AZ-623).
- New PBI **AZ-625** ("AZ-618 Phase E.5: c5_isam2_graph_handle ordering", 3pt, parent AZ-618) holds the handle work.
- **AZ-624** dependency edge updated to require both AZ-623 AND AZ-625.
## Files Changed
### Production
- `src/gps_denied_onboard/runtime_root/airborne_bootstrap.py` — added imports (`json`, `pathlib.Path`, `numpy`, `_types.calibration.CameraCalibration`, `helpers.imu_preintegrator.{ImuPreintegrator, make_imu_preintegrator}`, `helpers.ransac_filter.RansacFilter`, `helpers.wgs_converter.WgsConverter`); added module-level `_IMU_PREINTEGRATOR_CACHE` dict + `clear_imu_preintegrator_cache()` helper; added `_load_camera_calibration` (mirrors `_replay_branch._load_camera_calibration` but raises `AirborneBootstrapError`); added 4 builders `_build_c282_ransac_filter`, `_build_c5_imu_preintegrator` (cached on calibration path), `_build_c5_se3_utils` (returns the `helpers.se3_utils` module as a namespace handle, matching existing C5 estimator's MagicMock fixture pattern), `_build_c5_wgs_converter`; extended `build_pre_constructed` to populate the 4 new keys after the existing AZ-619..AZ-622 keys.
### Tests
- `tests/unit/runtime_root/test_az623_pre_constructed_phase_e.py` (NEW, 7 tests):
- `test_ac_623_1_adds_c282_ransac_and_c5_helpers` — AC-623.1 (4 keys + correct types).
- `test_ac_623_1_keeps_existing_keys_intact` — additivity invariant (AZ-619..AZ-622 keys still present).
- `test_ac_623_2_imu_preintegrator_cached_across_calls` — AC-623.2 (cache short-circuit via per-test counter).
- `test_ac_623_2_imu_preintegrator_per_path_cache` — AC-623.2 (per-path cache isolation).
- `test_ac_623_3_empty_calibration_path_raises_named_error` — AC-623.3 (operator-actionable error).
- `test_ac_623_3_unreadable_calibration_path_raises_named_error` — AC-623.3 (file not found path).
- `test_ac_623_3_malformed_json_raises_named_error` — AC-623.3 (JSON decode wrap).
- `tests/unit/runtime_root/test_az619_pre_constructed_phase_a.py` — added autouse `_stub_c5_builders`.
- `tests/unit/runtime_root/test_az620_pre_constructed_phase_b.py` — added autouse `_stub_c5_builders`.
- `tests/unit/runtime_root/test_az621_pre_constructed_phase_c.py` — added autouse `_stub_c5_builders`.
- `tests/unit/runtime_root/test_az622_pre_constructed_phase_d.py` — added autouse `_stub_c5_builders`.
### Specs
- `_docs/02_tasks/todo/AZ-623_*.md` → narrowed (handle work removed); ARCHIVED to `_docs/02_tasks/done/`.
- `_docs/02_tasks/todo/AZ-624_*.md` — Dependencies updated (AZ-625 added).
- `_docs/02_tasks/todo/AZ-625_c5_isam2_graph_handle_ordering.md` (NEW) — 3pt, parent AZ-618, holds the handle ordering work.
## AC Test Coverage: 4 of 4 covered
| AC | Test | Status |
|----|------|--------|
| AC-623.1 | `test_ac_623_1_adds_c282_ransac_and_c5_helpers` + `test_ac_623_1_keeps_existing_keys_intact` | Covered |
| AC-623.2 | `test_ac_623_2_imu_preintegrator_cached_across_calls` + `test_ac_623_2_imu_preintegrator_per_path_cache` | Covered |
| AC-623.3 | `test_ac_623_3_empty_calibration_path_raises_named_error` (+ 2 variants for unreadable/malformed) | Covered |
| AC-623.4 | File `tests/unit/runtime_root/test_az623_pre_constructed_phase_e.py` exists | Covered |
## Code Review Verdict: PASS_WITH_WARNINGS
Full report: `_docs/03_implementation/reviews/batch_94_review.md`. Three Low findings:
1. **F1 (Low / Maintainability)**`_load_camera_calibration` duplicates `_replay_branch._load_camera_calibration` (different exception class). Defer to a hygiene PBI (~2pt) bundled with batch 93's F2 leftover.
2. **F2 (Low / Maintainability)** — empty-path check duplicated across `_build_c5_imu_preintegrator` and `_load_camera_calibration` (defense-in-depth). No change recommended.
3. **F3 (Low / Style)**`c5_se3_utils` returns a Python module as a namespace handle. Documented; matches existing C5 test fixture pattern. YAGNI — defer Protocol introduction.
No Critical / High / Medium findings. Auto-fix not invoked.
## Auto-Fix Attempts: 0
## Stuck Agents: None
## Test Run Summary
- Targeted test set (AZ-619..AZ-623): **24 passed** in 1.07s.
- Regression check (`tests/unit/runtime_root/` + `tests/unit/c5_state/`): **247 passed** in 1.78s.
## Scope-Split Decision Trail
The split was driven by an irreconcilable construction-order issue between two umbrella constraints:
- **Constraint A** (umbrella AZ-618): "MUST NOT touch any per-component factory signature" + "All changes confined to runtime_root/airborne_bootstrap.py, runtime_root/__init__.py, and the new test file."
- **Constraint B** (existing seam): `build_state_estimator` returns `(StateEstimator, ISam2GraphHandle)` as a tuple — the handle is a structural side-product of the estimator, not separately constructable.
- **Constraint C** (existing seam): C4's `build_pose_estimator` consumes `c5_isam2_graph_handle` from `pre_constructed`, so `compose_root` topologically requires it to exist before C4 runs, which is before C5 runs.
Possible resolutions all touched at least one of A/B/C; the user-approved Option B preserves all three constraints by:
- Keeping the **C4 ↔ C5 seam intact** (no Protocol changes).
- Promoting the (estimator, handle) build into `airborne_bootstrap`, where AZ-618 explicitly permits orchestration.
- Documenting the strategy in AZ-625's spec § "Decision" with full rationale.
## Tier-2 / Deferred Work
- AZ-625 (3pt) — handle ordering work, blocks AZ-624.
- Hygiene PBI to consolidate `_load_camera_calibration` between airborne_bootstrap and `_replay_branch` (F1) and `_is_build_flag_on` triple-duplication (batch 93 F2). Bundle as ~2pt cleanup PBI post-AZ-618.
## Next Batch
- **Batch 95**: AZ-625 (Phase E.5: c5_isam2_graph_handle ordering) — 3pt.
- **Then Batch 96**: AZ-624 (Phase F: wire main() + AC-1..AC-5 verification incl. Jetson tier-2) — 2pt.
- **Cumulative review window**: next due at batch 96 (K=3 from last cumulative at 88-92).
@@ -0,0 +1,93 @@
# Code Review Report
**Batch**: 94
**Tasks**: AZ-623 (Phase E narrowed: c282_ransac_filter + c5_imu_preintegrator + c5_se3_utils + c5_wgs_converter; original `c5_isam2_graph_handle` work split out to AZ-625)
**Date**: 2026-05-19
**Verdict**: PASS_WITH_WARNINGS
## Phase 1: Context
Read in this review window:
- `_docs/02_tasks/todo/AZ-623_pre_constructed_phase_e_ransac_c5_helpers.md` (narrowed scope, 4 helpers; `c5_isam2_graph_handle` deferred to AZ-625 with full scope-split note)
- `_docs/02_tasks/todo/AZ-624_pre_constructed_phase_f_wire_main.md` (dependency edge updated: AZ-624 now blocks on BOTH AZ-623 and AZ-625)
- `_docs/02_tasks/todo/AZ-625_c5_isam2_graph_handle_ordering.md` (NEW — captures the deferred handle-ordering work)
- `_docs/02_tasks/todo/AZ-618_airborne_bootstrap_pre_constructed.md` (umbrella; constraints "MUST NOT touch any per-component factory signature" + "All changes confined to runtime_root/airborne_bootstrap.py, runtime_root/__init__.py, and the new test file")
- `src/gps_denied_onboard/helpers/{ransac_filter,imu_preintegrator,se3_utils,wgs_converter}.py` (helpers being wired)
- `src/gps_denied_onboard/runtime_root/_replay_branch.py` (existing `_load_camera_calibration` pattern — template for the airborne bootstrap loader)
- `src/gps_denied_onboard/components/c5_state/_isam2_handle.py` (Protocol seam constraint that drove the AZ-625 split)
- `src/gps_denied_onboard/runtime_root/state_factory.py` (build_state_estimator return-tuple — confirms seam)
- `src/gps_denied_onboard/runtime_root/__init__.py` (compose_root pre_constructed merge — confirms construction-order issue)
The autodev orchestrator escalated the `c5_isam2_graph_handle` ordering question via the AZ-623 spec's own escalation gate; the user chose Option B (split scope; new PBI for handle wiring). This review covers the narrowed AZ-623 only.
## Phase 2: Spec Compliance
| AC | Status | Test | Notes |
|----|--------|------|-------|
| AC-623.1 (4 keys added on top of AZ-619..AZ-622) | Covered | `test_ac_623_1_adds_c282_ransac_and_c5_helpers` + `test_ac_623_1_keeps_existing_keys_intact` | Each new key is type-asserted; `c5_se3_utils` is the helpers.se3_utils module (matches existing C5 estimator's MagicMock fixture pattern via attribute access). |
| AC-623.2 (`c5_imu_preintegrator` cached per `camera_calibration_path`; stateless 3 may be fresh) | Covered | `test_ac_623_2_imu_preintegrator_cached_across_calls` + `test_ac_623_2_imu_preintegrator_per_path_cache` | Cache short-circuit verified by counter; per-path isolation verified by two distinct paths producing distinct instances. |
| AC-623.3 (operator-actionable error on missing/bad calibration) | Covered | `test_ac_623_3_empty_calibration_path_raises_named_error` + `_unreadable_` + `_malformed_json_` | All three error paths assert the message names `c5_imu_preintegrator`, the missing-input symptom, and the consuming component slug `c5_state`. Mirrors AZ-622's `c3_lightglue_runtime` error-message contract. |
| AC-623.4 (test file exists) | Covered | `tests/unit/runtime_root/test_az623_pre_constructed_phase_e.py` | 7 tests, all passing. |
**Spec drift / scope split**: The user-approved scope split (handle work → AZ-625) is documented in three places — AZ-623 spec § "Scope split note", AZ-625 spec § "Why split out of AZ-623", AZ-624 Dependencies update. The Jira `addCommentToJiraIssue` write on AZ-623 also captures the split. No silent drift.
## Phase 3: Code Quality
3 findings — all Low; none blocking:
- F1 (Low / Maintainability): `_load_camera_calibration` (airborne_bootstrap) duplicates `_load_camera_calibration` (`_replay_branch.py`). The bodies share the JSON shape and field defaults; they differ only in exception class (`AirborneBootstrapError` here vs `CompositionError` in replay) and the `airborne-camera` vs `replay-camera` default `camera_id`. The umbrella's "MUST be confined to runtime_root/airborne_bootstrap.py" constraint nominally permits the duplication, but a future hygiene PBI should consolidate the JSON-decode core into a shared helper that takes the exception class as a callable parameter.
- F2 (Low / Maintainability): empty-path check duplicated in `_build_c5_imu_preintegrator` AND `_load_camera_calibration`. Intentional defense-in-depth (the loader is a public-ish seam tests monkeypatch directly, so it must validate independently); the upper check enables a tighter cache lookup before the loader fires. Documented in both docstrings. No change recommended unless the seam is removed in a future refactor.
- F3 (Low / Style): `c5_se3_utils` returns a Python module (`gps_denied_onboard.helpers.se3_utils`) as a namespace handle. Unusual at first glance but matches the existing `MagicMock()` fixture pattern in `tests/unit/c5_state/test_az386_eskf_baseline.py`; consumers store as `self._se3_utils: Any` and dispatch via attribute access. The docstring spells this out and references the existing C5 test pattern. A future enhancement could introduce a `Se3UtilsHandle` Protocol if the inferred shape grows, but YAGNI applies here.
## Phase 4: Security
No findings.
- No SQL, no command exec, no `eval`/`exec`.
- JSON parsing uses `json.loads` (safe) on file content.
- File paths flow from `config.runtime.camera_calibration_path` (operator input via YAML, not user-provided per request); `Path(path).read_text(encoding="utf-8")` reads the file with explicit encoding — no path traversal, no encoding ambiguity.
- No secrets in error messages: the path string is included for operator-actionability, but the file content is never echoed.
## Phase 5: Performance
No findings.
- `_IMU_PREINTEGRATOR_CACHE` is a single-key dict lookup per `build_pre_constructed` call. The expensive path (JSON read + GTSAM `PreintegrationCombinedParams` construction) fires at most once per process per calibration path.
- The 3 stateless helpers (`RansacFilter()`, `WgsConverter()`, the `se3_utils` module reference) are O(1) constructions.
- Bootstrap is a startup path — no latency-budget concerns at this layer beyond AZ-618's NFR (60s on Jetson Orin Nano), which is dominated by C7 GPU model load (AZ-621), not the AZ-623 helpers.
## Phase 6: Cross-Task Consistency
- Autouse stubs added to test_az619 / test_az620 / test_az621 / test_az622 with consistent pattern (named lambdas returning `MagicMock(name="...")` or `object()` sentinels). Each fixture explains why it's needed and what it stubs. No drift in stub style across the 4 prior phase test files.
- Error-message contract for `AirborneBootstrapError` follows the AZ-622 pattern (names the missing key, the gating input, and the consuming component slug).
- `c5_se3_utils` module-as-namespace pattern is documented in both the production code and the test suite.
## Phase 7: Architecture Compliance
No findings.
- New imports in `airborne_bootstrap.py`: `json` (stdlib), `pathlib.Path` (stdlib), `numpy` (stack-wide), `gps_denied_onboard._types.calibration.CameraCalibration` (cross-cutting types — bootstrap may import any `_types` per AZ-507), `gps_denied_onboard.helpers.{imu_preintegrator,ransac_filter,wgs_converter}` (CC-HELPERS layer; bootstrap may import any helper). All within the bootstrap's allowed dependency surface per `_docs/02_document/module-layout.md`.
- No new cyclic dependencies: `airborne_bootstrap` imports from helpers + types + factories; helpers import from `_types`; no back-edge introduced.
- No Public API bypass: every imported symbol is in the helper module's documented `__all__` or is a stdlib-equivalent re-export.
- The umbrella's "MUST NOT touch any per-component factory signature" constraint is honored: zero edits to `state_factory.py`, `pose_factory.py`, or any `c5_state` / `c4_pose` internal.
- The umbrella's "All changes confined to runtime_root/airborne_bootstrap.py, runtime_root/__init__.py, and the new test file" constraint is honored: production-side changes are only in `airborne_bootstrap.py` (this batch did not touch `runtime_root/__init__.py` — AZ-624's job).
## Findings Summary
| # | Severity | Category | File:Line | Title |
|---|----------|----------|-----------|-------|
| F1 | Low | Maintainability | `airborne_bootstrap.py` (`_load_camera_calibration`) | Duplicates `_replay_branch._load_camera_calibration` |
| F2 | Low | Maintainability | `airborne_bootstrap.py` (`_build_c5_imu_preintegrator` + `_load_camera_calibration`) | Defense-in-depth duplicates the empty-path check |
| F3 | Low | Style | `airborne_bootstrap.py` (`_build_c5_se3_utils`) | Returns a module as a namespace handle |
Verdict: **PASS_WITH_WARNINGS** — 0 Critical, 0 High, 0 Medium, 3 Low. Auto-fix gate not triggered.
## Test Run
Targeted: `tests/unit/runtime_root/test_az619..623`**24 passed in 2.78s**.
Regression: `tests/unit/runtime_root/ tests/unit/c5_state/`**247 passed in 1.41s**.
## Suggested Follow-Ups (informational)
- Hygiene PBI (~2pt): consolidate `_load_camera_calibration` between airborne_bootstrap and `_replay_branch` into a shared `runtime_root/_camera_calibration_loader.py` that accepts an exception-class parameter. Mirrors the F2 of batch 93 (`_is_build_flag_on` triple-duplication PBI). The two leftover hygiene PBIs (F2-batch93 + this F1-batch94) could land together.