mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 05:01:14 +00:00
[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:
@@ -0,0 +1,63 @@
|
||||
# AZ-623 — Phase E: build_pre_constructed seeds c282_ransac_filter + 3 c5 helpers
|
||||
|
||||
**Task**: AZ-623_pre_constructed_phase_e_ransac_c5_helpers
|
||||
**Name**: AZ-618 Phase E: build_pre_constructed seeds c282_ransac_filter + c5 helpers
|
||||
**Description**: Fifth subtask of AZ-618. Extends `airborne_bootstrap.build_pre_constructed(config)` to populate the small / stateless helper entries.
|
||||
**Complexity**: 3 points
|
||||
**Dependencies**: AZ-619, AZ-282 (RansacFilter), AZ-276 (ImuPreintegrator), AZ-277 (SE3Utils), AZ-279 (WgsConverter). All in `done/`.
|
||||
**Component**: runtime_root (cross-cutting)
|
||||
**Tracker**: AZ-623
|
||||
**Epic**: AZ-602 (parent: AZ-618 umbrella)
|
||||
|
||||
## Scope split note (2026-05-19)
|
||||
|
||||
The original AZ-623 included `c5_isam2_graph_handle`. Path 1 of the spec's two-path investigation requires a Protocol seam change to `ISam2GraphHandleImpl` (handle is tightly coupled to `GtsamIsam2StateEstimator` via constructor injection); the AZ-618 umbrella forbids per-component factory signature changes. Per the spec's own escalation gate, the user was asked and chose to split: this ticket lands the 4 stateless helpers; AZ-625 (`AZ-618 follow-up: c5_isam2_graph_handle ordering`) lands the handle wiring with a documented in-bootstrap (estimator, handle) build + look-aside short-circuit pattern. AZ-624 (main wiring + AC verification) now blocks on BOTH AZ-623 and AZ-625.
|
||||
|
||||
## Outcome
|
||||
|
||||
- `build_pre_constructed(config)` adds keys `c282_ransac_filter`, `c5_imu_preintegrator`, `c5_se3_utils`, `c5_wgs_converter` on top of AZ-619..AZ-622. (`c5_isam2_graph_handle` moved to AZ-625.)
|
||||
- New unit tests under `tests/unit/runtime_root/test_az623_pre_constructed_phase_e.py`.
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
|
||||
- Internal builders for the 4 keys above. `c282_ransac_filter` and `c5_wgs_converter` return fresh stateless instances (`RansacFilter()`, `WgsConverter()`); `c5_se3_utils` returns the `gps_denied_onboard.helpers.se3_utils` module (consumers access functions as attributes; tests stub via `MagicMock` with the same attribute-access shape); `c5_imu_preintegrator` builds via `make_imu_preintegrator(camera_calibration)` with the calibration loaded from `config.runtime.camera_calibration_path` and the result cached per path.
|
||||
- The calibration loader function in `airborne_bootstrap.py` mirrors the JSON shape `runtime_root._replay_branch._load_camera_calibration` already uses — same on-disk format, no new file format introduced.
|
||||
- Unit tests covering AC-623.1 + AC-623.2.
|
||||
|
||||
### Excluded
|
||||
|
||||
- main() wiring (AZ-624).
|
||||
- GPU-touching builders (AZ-621 / AZ-622).
|
||||
- `c5_isam2_graph_handle` seeding (AZ-625, see Scope split note).
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
**AC-623.1**: `build_pre_constructed(config)` adds the 4 keys above on top of AZ-619..AZ-622.
|
||||
|
||||
**AC-623.2**: invoking `build_pre_constructed(config)` twice within the same process produces a dict where `c5_imu_preintegrator` is the same object both times (cached by `config.runtime.camera_calibration_path`). The 3 stateless helpers may be either fresh or cached — caching is irrelevant for them.
|
||||
|
||||
**AC-623.3**: when `config.runtime.camera_calibration_path` is empty or unreadable, `_build_c5_imu_preintegrator` raises `AirborneBootstrapError` naming the missing input and the consuming component slug `c5_state` (operator-actionable error mirroring AC-3 / AC-622.2).
|
||||
|
||||
**AC-623.4**: `pytest tests/unit/runtime_root/test_az623_pre_constructed_phase_e.py` covers AC-623.1 + AC-623.2 + AC-623.3.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `c5_se3_utils` returns the `helpers.se3_utils` MODULE (Python modules support attribute access for their public names; consumers store it as `self._se3_utils: Any` and call `self._se3_utils.exp_map(...)`). The `MagicMock()` fixture used by existing C5 estimator tests has the same shape.
|
||||
- `c5_wgs_converter` returns `WgsConverter()` (instance of a static-only class) — same pattern `runtime_root._replay_branch.run` already uses (`wgs_converter = WgsConverter()`).
|
||||
- `c282_ransac_filter` returns `RansacFilter()` (instance of a static-only class) — consumers (`c3_matcher`, `c3_5_adhop`, `c4_pose`) use it for attribute-dispatch only.
|
||||
- `c5_imu_preintegrator` is the only stateful helper; its bias accumulator survives across `build_pre_constructed` invocations, hence the per-path cache.
|
||||
|
||||
## Constraints
|
||||
|
||||
- MUST NOT modify per-component factory signatures.
|
||||
- MUST be additive on top of AZ-619..AZ-622.
|
||||
|
||||
## Evidence
|
||||
|
||||
- Umbrella spec: `_docs/02_tasks/todo/AZ-618_airborne_bootstrap_pre_constructed.md`
|
||||
- Helper modules: `src/gps_denied_onboard/helpers/{ransac_filter,imu_preintegrator,se3_utils,wgs_converter}.py`
|
||||
- `c5_state` factory: `src/gps_denied_onboard/runtime_root/state_factory.py`
|
||||
- ISam2GraphHandle (deferred to AZ-625): `src/gps_denied_onboard/components/c5_state/_isam2_handle.py`
|
||||
- Calibration loader pattern: `runtime_root/_replay_branch.py::_load_camera_calibration`
|
||||
@@ -1,58 +0,0 @@
|
||||
# AZ-623 — Phase E: build_pre_constructed seeds c282_ransac_filter + c5 helpers
|
||||
|
||||
**Task**: AZ-623_pre_constructed_phase_e_ransac_c5_helpers
|
||||
**Name**: AZ-618 Phase E: build_pre_constructed seeds c282_ransac_filter + c5 helpers
|
||||
**Description**: Fifth subtask of AZ-618. Extends `airborne_bootstrap.build_pre_constructed(config)` to populate the small / stateless helper entries plus the special-case `c5_isam2_graph_handle`.
|
||||
**Complexity**: 3 points
|
||||
**Dependencies**: AZ-619, AZ-282 (RansacFilter), AZ-276 (ImuPreintegrator), AZ-277 (SE3Utils), AZ-279 (WgsConverter), AZ-381 (ISam2GraphHandle). All in `done/`.
|
||||
**Component**: runtime_root (cross-cutting)
|
||||
**Tracker**: AZ-623
|
||||
**Epic**: AZ-602 (parent: AZ-618 umbrella)
|
||||
|
||||
## Outcome
|
||||
|
||||
- `build_pre_constructed(config)` adds keys `c282_ransac_filter`, `c5_imu_preintegrator`, `c5_se3_utils`, `c5_wgs_converter`, `c5_isam2_graph_handle` on top of AZ-619..AZ-622.
|
||||
- The `c5_isam2_graph_handle` ordering question (C4 wrapper consumes it BEFORE C5 wrapper runs in the topo order) is resolved here — not deferred to AZ-624.
|
||||
- New unit tests under `tests/unit/runtime_root/test_az623_pre_constructed_phase_e.py`.
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
|
||||
- Internal builders for the 5 keys above. Most are trivial constructions (`RansacFilter()`, `WgsConverter()`); `c5_imu_preintegrator` uses `make_imu_preintegrator(camera_calibration)`; `c5_se3_utils` is a thin namespace object exposing the module functions; `c5_isam2_graph_handle` is the per-strategy build (gtsam_isam2 / eskf).
|
||||
- Resolution of the C4-before-C5 ordering for `c5_isam2_graph_handle`. Investigation budget ≤1 hour.
|
||||
- Unit tests covering AC-623.1 + AC-623.2.
|
||||
|
||||
### Excluded
|
||||
|
||||
- main() wiring (AZ-624).
|
||||
- Any GPU-touching builders (AZ-621 / AZ-622).
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
**AC-623.1**: `build_pre_constructed(config)` adds the 5 keys above on top of AZ-619..AZ-622.
|
||||
|
||||
**AC-623.2**: invoking twice produces dicts where stateless helpers (`c282_ransac_filter`, `c5_wgs_converter`, `c5_se3_utils`) may be either fresh or cached; `c5_imu_preintegrator` and `c5_isam2_graph_handle` MUST be the same instance across calls within the same process (cached) — this matches the per-component factory's identity expectations.
|
||||
|
||||
**AC-623.3**: `pytest tests/unit/runtime_root/test_az623_pre_constructed_phase_e.py` covers AC-623.1 + AC-623.2.
|
||||
|
||||
## Implementation Note
|
||||
|
||||
The `c5_isam2_graph_handle` / `build_state_estimator` ordering question is the only non-trivial bit here. `build_state_estimator` returns `(StateEstimator, ISam2GraphHandle)` together — but the airborne wrapper for c4_pose consumes `c5_isam2_graph_handle` from `pre_constructed` BEFORE the c5_state wrapper runs in the topo order. Two paths to investigate:
|
||||
|
||||
1. Construct the handle here in the bootstrap (separately from the StateEstimator) and put it in `pre_constructed["c5_isam2_graph_handle"]`. Then the `_c5_state_wrapper` consumes the SAME handle from `constructed` (which gets seeded from `pre_constructed`) instead of building a new one.
|
||||
2. Reorder the bootstrap to run c5_state first (before c4_pose) so the StateEstimator + handle are built together — but this conflicts with the existing `_C5_STATE_DEPENDS_ON: ("c1_vio", "c4_pose")` declaration.
|
||||
|
||||
Path 1 is preferred (no topology change). Path 2 requires editing `_AIRBORNE_REGISTRATIONS` and is potentially a larger change. Spend ≤1 hour deciding; if Path 1's separation requires a Protocol seam change to ISam2GraphHandle's construction, escalate to the user (do NOT defer to AZ-624).
|
||||
|
||||
## Constraints
|
||||
|
||||
- MUST NOT modify per-component factory signatures.
|
||||
- MUST be additive on top of AZ-619..AZ-622.
|
||||
|
||||
## Evidence
|
||||
|
||||
- Umbrella spec: `_docs/02_tasks/todo/AZ-618_airborne_bootstrap_pre_constructed.md`
|
||||
- Helper modules: `src/gps_denied_onboard/helpers/{ransac_filter,imu_preintegrator,se3_utils,wgs_converter}.py`
|
||||
- `c5_state` factory: `src/gps_denied_onboard/runtime_root/state_factory.py`
|
||||
- ISam2GraphHandle: `src/gps_denied_onboard/components/c5_state/_isam2_handle.py`
|
||||
@@ -4,7 +4,7 @@
|
||||
**Name**: AZ-618 Phase F: wire build_pre_constructed into runtime_root.main() + AC-1..AC-5 verification
|
||||
**Description**: Final / umbrella subtask of AZ-618. Wires `build_pre_constructed` into `runtime_root.main()` and lands the FULL AC suite from the AZ-618 umbrella (AC-1..AC-5), including the Jetson tier-2 verification.
|
||||
**Complexity**: 2 points
|
||||
**Dependencies**: AZ-619, AZ-620, AZ-621, AZ-622, AZ-623 (all phases must be in `done/` before this lands).
|
||||
**Dependencies**: AZ-619, AZ-620, AZ-621, AZ-622, AZ-623, AZ-625 (all phases must be in `done/` before this lands; AZ-625 was added 2026-05-19 when the `c5_isam2_graph_handle` ordering work was split out of AZ-623).
|
||||
**Component**: runtime_root (cross-cutting)
|
||||
**Tracker**: AZ-624
|
||||
**Epic**: AZ-602 (parent: AZ-618 umbrella)
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# AZ-625 — Phase E.5: airborne_bootstrap c5_isam2_graph_handle ordering
|
||||
|
||||
**Task**: AZ-625_c5_isam2_graph_handle_ordering
|
||||
**Name**: AZ-618 follow-up: c5_isam2_graph_handle ordering — separate handle from estimator construction
|
||||
**Description**: Sixth subtask of AZ-618 (added 2026-05-19 by autodev batch 94 escalation). Lands the `pre_constructed["c5_isam2_graph_handle"]` seeding that AZ-623 originally listed but escalated out of scope when Path 1 of the AZ-623 spec's two-path investigation required a Protocol seam change forbidden by the AZ-618 umbrella.
|
||||
**Complexity**: 3 points
|
||||
**Dependencies**: AZ-619..AZ-623 (all in `done/` before this lands).
|
||||
**Component**: runtime_root (cross-cutting)
|
||||
**Tracker**: AZ-625
|
||||
**Epic**: AZ-602 (parent: AZ-618 umbrella)
|
||||
|
||||
## Why split out of AZ-623
|
||||
|
||||
`compose_root` walks topo order and seeds `constructed` from `pre_constructed`. `c4_pose` pulls `c5_isam2_graph_handle` from `constructed` at construction time. `_C5_STATE_DEPENDS_ON: ("c1_vio", "c4_pose")` puts c5 AFTER c4 in topo. So at c4_pose's construction time, `c5_isam2_graph_handle` is not in `constructed` unless seeded by `pre_constructed`.
|
||||
|
||||
`ISam2GraphHandleImpl.__init__(self, estimator: GtsamIsam2StateEstimator)` ties handle construction to estimator construction. Building the handle separately requires a Protocol seam change — explicitly forbidden by the AZ-618 umbrella's "MUST NOT touch any per-component factory signature" constraint.
|
||||
|
||||
`_c5_state_wrapper` currently builds the estimator + handle together via `build_state_estimator`, discards the handle (`estimator, _handle = ...`), and returns the estimator. The handle never reaches `pre_constructed`.
|
||||
|
||||
## Decision (2026-05-19, autodev batch 94)
|
||||
|
||||
Path 1 (handle-only separation) is blocked. Chosen approach: a wiring change inside `airborne_bootstrap.py` only.
|
||||
|
||||
- `airborne_bootstrap.build_pre_constructed` calls `build_state_estimator` once, captures `(estimator, handle)`, and seeds:
|
||||
- `pre_constructed["c5_isam2_graph_handle"] = handle`
|
||||
- a private coordination key, e.g. `pre_constructed["_c5_prebuilt_estimator"] = estimator`
|
||||
- `_c5_state_wrapper` consults `constructed.get("_c5_prebuilt_estimator")` and returns the prebuilt instance when present; otherwise falls back to `build_state_estimator(...)` (preserves test isolation for fixtures that don't go through the bootstrap).
|
||||
- No changes to `state_factory.build_state_estimator` signature, `ISam2GraphHandleImpl.__init__`, or the C4 / C5 Protocol declarations.
|
||||
|
||||
## Outcome
|
||||
|
||||
- `build_pre_constructed(config)` adds `c5_isam2_graph_handle` on top of AZ-619..AZ-623, and the c4_pose wrapper sees the same handle the c5_state estimator was built with.
|
||||
- `_c5_state_wrapper` short-circuits on the look-aside key.
|
||||
- New unit tests under `tests/unit/runtime_root/test_az625_c5_isam2_graph_handle_ordering.py`.
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
|
||||
- `airborne_bootstrap.py`: new internal builder `_build_c5_state_estimator_pair(config, ...)` that calls `build_state_estimator` once and returns `(estimator, handle)`. `build_pre_constructed` invokes it and seeds both keys.
|
||||
- `airborne_bootstrap.py`: `_c5_state_wrapper` short-circuits on `constructed["_c5_prebuilt_estimator"]`.
|
||||
- Document `_c5_prebuilt_estimator` as an internal coordination key (NOT in `AIRBORNE_REQUIRED_PRE_CONSTRUCTED_KEYS` because consumers don't query it).
|
||||
- `C5_STATE_BUILD_FLAGS: Final[Mapping[str, str]]` mirroring `state_factory._STATE_BUILD_FLAGS` so the bootstrap can name the gating flag in `AirborneBootstrapError` (mirrors AZ-622's `C3_MATCHER_BUILD_FLAGS` pattern).
|
||||
- Unit tests covering AC-625.1..AC-625.3.
|
||||
|
||||
### Excluded
|
||||
|
||||
- Any change to `state_factory`, `pose_factory`, or `c5_state` internals.
|
||||
- main() wiring (still AZ-624's job; AZ-624 now depends on this PBI).
|
||||
- AZ-389 orthorectifier wiring (`camera_calibration` / `flight_id` / `companion_id` flow through `pre_constructed`) — orthogonal; lands as part of AZ-624 or its own follow-up.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
**AC-625.1**: `build_pre_constructed(config)` adds key `c5_isam2_graph_handle` on top of AZ-619..AZ-623; the value satisfies the C4 `ISam2GraphHandle` Protocol (`get_pose_key`, `add_factor`, `update`, `compute_marginals`, `last_anchor_age_ms`).
|
||||
|
||||
**AC-625.2**: When the configured `c5_state` strategy's `BUILD_STATE_*` flag is OFF (or the strategy is unknown), `build_pre_constructed` raises `AirborneBootstrapError` naming the missing flag and the consuming component slug `c5_state`. (Mirrors the AC-3 / AC-622.2 error contract.)
|
||||
|
||||
**AC-625.3**: `compose_root(config, pre_constructed=build_pre_constructed(config))` produces a runtime where the handle held by c4_pose IS the same handle returned by the c5_state estimator's `_isam2_handle`. Identity assertion verifies single-instance sharing across the C4 / C5 seam.
|
||||
|
||||
**AC-625.4**: Unit tests under `tests/unit/runtime_root/test_az625_c5_isam2_graph_handle_ordering.py` cover AC-625.1..AC-625.3.
|
||||
|
||||
## Tier-2 Note
|
||||
|
||||
Real iSAM2 graph mutations under load are verified by AZ-624's Jetson AC-5 run.
|
||||
|
||||
## Constraints
|
||||
|
||||
- MUST NOT modify per-component factory signatures.
|
||||
- MUST be additive on top of AZ-619..AZ-623.
|
||||
- MUST NOT touch `state_factory`, `pose_factory`, or `c5_state` internals.
|
||||
|
||||
## Evidence
|
||||
|
||||
- Umbrella spec: `_docs/02_tasks/todo/AZ-618_airborne_bootstrap_pre_constructed.md`
|
||||
- Original AZ-623 spec (now narrowed): `_docs/02_tasks/todo/AZ-623_pre_constructed_phase_e_ransac_c5_helpers.md` § "Scope split note"
|
||||
- ISam2GraphHandle Protocol (consumed by C4): `src/gps_denied_onboard/components/c4_pose/_isam2_handle.py`
|
||||
- ISam2GraphHandleImpl (built by C5 estimator): `src/gps_denied_onboard/components/c5_state/_isam2_handle.py`
|
||||
- `state_factory.build_state_estimator`: `src/gps_denied_onboard/runtime_root/state_factory.py`
|
||||
- AZ-622's `C3_MATCHER_BUILD_FLAGS` pattern (template for `C5_STATE_BUILD_FLAGS`): `src/gps_denied_onboard/runtime_root/airborne_bootstrap.py`
|
||||
@@ -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.
|
||||
@@ -8,8 +8,8 @@ status: in_progress
|
||||
sub_step:
|
||||
phase: 16
|
||||
name: batch-loop
|
||||
detail: "batch 93 done; next: batch 94 = AZ-623 (Phase E, 3cp)"
|
||||
detail: "batch 94 done; next: batch 95 = AZ-625 (handle ordering, 3cp); AZ-624 blocked on AZ-625"
|
||||
retry_count: 0
|
||||
cycle: 1
|
||||
tracker: jira
|
||||
last_completed_batch: 93
|
||||
last_completed_batch: 94
|
||||
|
||||
Reference in New Issue
Block a user