# Batch Report

**Batch**: 95
**Tasks**: AZ-625 (Phase E.5 of AZ-618: c5_isam2_graph_handle ordering — separate handle from estimator construction)
**Date**: 2026-05-19
**Cycle**: 1

## Task Results

| Task | Status | Files Modified | Tests | AC Coverage | Issues |
|------|--------|----------------|-------|-------------|--------|
| AZ-625_c5_isam2_graph_handle_ordering | Done | 7 files | 8 new + 24 carry-over | 4/4 ACs covered | 0 blocking (2 Low / Style + Maintainability — accepted per code review) |

AZ-625 was split out of AZ-623 in batch 94 after the AZ-623 task spec's two-path investigation surfaced an unresolvable construction-order conflict (c4_pose consumes `c5_isam2_graph_handle` from `pre_constructed`; the handle is built inside `build_state_estimator`'s tuple return, which c5_state's wrapper invokes — but the c5_state wrapper runs AFTER c4_pose in topological order). Path 1 (handle-only separation) required a Protocol-seam change in C5 — explicitly forbidden by the AZ-618 umbrella's "MUST NOT touch any per-component factory signature" constraint. Path 2 (eager `(estimator, handle)` build at bootstrap) is the chosen approach: this batch lands it.

## Files Changed

### Production

- `src/gps_denied_onboard/runtime_root/airborne_bootstrap.py`:
  - Added `C5_STATE_BUILD_FLAGS: Final[Mapping[str, str]]` constant mirroring `state_factory._STATE_BUILD_FLAGS` so the bootstrap can name the gating `BUILD_STATE_*` flag in operator errors.
  - Added `_resolve_c5_state_strategy(config)` mirroring `_resolve_c3_matcher_strategy` for symmetry.
  - Added `_C5_PREBUILT_ESTIMATOR_KEY: Final[str] = "_c5_prebuilt_estimator"` internal coordination key (deliberately NOT in `AIRBORNE_REQUIRED_PRE_CONSTRUCTED_KEYS` — only the C5 wrapper consults it as a fast path).
  - Added `_build_c5_state_estimator_pair(config, *, imu_preintegrator, se3_utils, wgs_converter, fdr_client, tile_store=None, camera_calibration=None, flight_id=None, companion_id=None)` that resolves the strategy, gates on the per-strategy `BUILD_STATE_*` flag, lazily registers the strategy via `_ensure_state_strategy_registered`, calls `build_state_estimator` once, captures the `(estimator, handle)` tuple, and wraps any `StateEstimatorConfigError` into `AirborneBootstrapError` with `__cause__` preserved.
  - Modified `_c5_state_wrapper` to short-circuit on `constructed.get(_C5_PREBUILT_ESTIMATOR_KEY)`: when the look-aside key is present, the prebuilt estimator is returned as-is; otherwise the wrapper falls through to the original `build_state_estimator` path (preserves test isolation for fixtures that bypass `build_pre_constructed`, e.g. `test_az401_compose_root_replay`).
  - Extended `build_pre_constructed` to call `_build_c5_state_estimator_pair` after the AZ-619..AZ-623 keys are populated and seed both `c5_isam2_graph_handle` (Public API key consumed by C4) and `_c5_prebuilt_estimator` (internal coordination key consumed by `_c5_state_wrapper`).
  - Updated `__all__` to export `C5_STATE_BUILD_FLAGS`.
  - Updated `build_pre_constructed`'s docstring to describe the AZ-625 wiring + error contract (eager-pair construction, look-aside key, BUILD-flag gating, error wrapping).

### Tests

- `tests/unit/runtime_root/test_az625_c5_isam2_graph_handle_ordering.py` (NEW, 8 tests):
  - `test_ac_625_1_adds_c5_isam2_graph_handle_with_protocol_surface` — AC-625.1 (key present + `isinstance(handle, C4ISam2GraphHandle)` + per-method `hasattr` + AZ-619..AZ-623 keys still present).
  - `test_ac_625_1_internal_prebuilt_estimator_key_not_in_required_keys` — internal-key invariant (the coordination key is NOT exposed via `AIRBORNE_REQUIRED_PRE_CONSTRUCTED_KEYS`).
  - `test_ac_625_2_build_state_gtsam_isam2_off_raises_named_error` — AC-625.2 (`BUILD_STATE_GTSAM_ISAM2=OFF` raises with the missing key + flag + `c5_state` slug; no upstream cause because the gate fires before the factory).
  - `test_ac_625_2_unknown_strategy_raises_named_error_with_supported_set` — AC-625.2 (unknown strategy raises with the supported strategy set named).
  - `test_ac_625_2_build_state_estimator_config_error_wraps_into_bootstrap_error` — defense-in-depth (`StateEstimatorConfigError` wraps into `AirborneBootstrapError` with `__cause__` preserved).
  - `test_ac_625_3_handle_is_same_object_as_estimator_isam2_handle` — AC-625.3 cross-seam identity (`pre_constructed[_c5_prebuilt_estimator]._isam2_handle IS pre_constructed['c5_isam2_graph_handle']`).
  - `test_ac_625_3_c5_state_wrapper_short_circuits_on_prebuilt_estimator` — wrapper short-circuit returns the prebuilt estimator without consulting the fallback infrastructure keys.
  - `test_ac_625_3_c5_state_wrapper_falls_back_when_prebuilt_absent` — wrapper falls back to `build_state_estimator` when the look-aside key is absent (preserves existing fixture behavior).

- `tests/unit/runtime_root/test_az619_pre_constructed_phase_a.py` — added autouse stub for `_build_c5_state_estimator_pair` (returns `(MagicMock, MagicMock)`) so AZ-619's bare `Config()` bootstrap path doesn't trip on the default `gtsam_isam2` strategy needing a real registered factory.
- `tests/unit/runtime_root/test_az620_pre_constructed_phase_b.py` — same autouse stub.
- `tests/unit/runtime_root/test_az621_pre_constructed_phase_c.py` — same autouse stub.
- `tests/unit/runtime_root/test_az622_pre_constructed_phase_d.py` — same autouse stub.
- `tests/unit/runtime_root/test_az623_pre_constructed_phase_e.py` — same autouse stub.

### Reviews

- `_docs/03_implementation/reviews/batch_95_review.md` (NEW) — code review report (verdict: PASS; 2 Low findings on style + function-scoped import).

### Specs

- `_docs/02_tasks/todo/AZ-625_*.md` → ARCHIVED to `_docs/02_tasks/done/`.

## AC Test Coverage

All 4 ACs covered:

| AC | Coverage |
|----|----------|
| AC-625.1 | `test_ac_625_1_adds_c5_isam2_graph_handle_with_protocol_surface` (presence + Protocol conformance) + `test_ac_625_1_internal_prebuilt_estimator_key_not_in_required_keys` |
| AC-625.2 | `test_ac_625_2_build_state_gtsam_isam2_off_raises_named_error` + `test_ac_625_2_unknown_strategy_raises_named_error_with_supported_set` + `test_ac_625_2_build_state_estimator_config_error_wraps_into_bootstrap_error` |
| AC-625.3 | `test_ac_625_3_handle_is_same_object_as_estimator_isam2_handle` + `test_ac_625_3_c5_state_wrapper_short_circuits_on_prebuilt_estimator` + `test_ac_625_3_c5_state_wrapper_falls_back_when_prebuilt_absent` |
| AC-625.4 | file `tests/unit/runtime_root/test_az625_c5_isam2_graph_handle_ordering.py` exists with the above tests |

## Test Run

| Suite | Result |
|-------|--------|
| `tests/unit/runtime_root/test_az619..test_az623 + test_az625` (targeted) | 32 passed in 1.18s |
| `tests/unit/runtime_root/ + tests/unit/c5_state/` (regression) | 255 passed in 1.38s |

No failures; no skips beyond pre-existing environment-gated tests.

## Code Review

- **Verdict**: PASS (0 Critical, 0 High, 0 Medium, 2 Low).
- F1 (Low / Style): `_C5_PREBUILT_ESTIMATOR_KEY` defined after first reference site — accepted (grouping with C5-state constants is the dominant readability axis; Python's lazy resolution makes this correct).
- F2 (Low / Maintainability): function-scoped `StateEstimatorConfigError` import — accepted (matches the file's existing function-scope-import convention for c5_state submodules).
- 0 auto-fix attempts; 0 escalated findings.

Full report: `_docs/03_implementation/reviews/batch_95_review.md`.

## Constraint Compliance (AZ-618 umbrella)

- "MUST NOT modify per-component factory signatures" → `state_factory.build_state_estimator` invoked with the same kwargs the wrapper would have passed; no signature changed. ✓
- "MUST be additive on top of AZ-619..AZ-623" → AZ-619..AZ-623 keys still present in `pre_constructed`. ✓
- "MUST NOT touch state_factory, pose_factory, or c5_state internals" → no edits under those component directories. ✓
- "All changes confined to `airborne_bootstrap.py` + new test file" → diff scope respected. The autouse-stub additions in AZ-619..AZ-623 phase tests are hygiene maintenance for the additive bootstrap call (each test now stubs the new `_build_c5_state_estimator_pair` builder), not a behavioral change to those phases' contracts. ✓

## Loop Status

- AZ-625 unblocks AZ-624 (the only remaining AZ-618 subtask). AZ-624's dependency edge in `_dependencies_table.md` lists `AZ-619..AZ-623, AZ-625` as upstream — all done. Next batch (96) will pick up AZ-624.
