Files
Oleksandr Bezdieniezhnykh 2b8ef52f66 [AZ-625] Phase E.5: airborne_bootstrap c5_isam2_graph_handle ordering
Wire the airborne bootstrap to seed pre_constructed['c5_isam2_graph_handle']
so c4_pose's compose-time lookup is satisfied (c4_pose runs before c5_state in
topological order; the iSAM2 graph handle is built INSIDE the C5 estimator's
constructor and so must be produced eagerly at bootstrap time).

build_pre_constructed now invokes a new internal _build_c5_state_estimator_pair
helper that calls state_factory.build_state_estimator once, captures the
(estimator, handle) tuple, and seeds two slots: 'c5_isam2_graph_handle' for
C4's lookup, and an internal '_c5_prebuilt_estimator' look-aside key for the
C5 wrapper's short-circuit. _c5_state_wrapper checks the look-aside key first
and returns the prebuilt instance as-is — the SAME object the handle was
extracted from, so c4_pose._isam2_handle and c5_state._isam2_handle reference
ONE object across the C4 / C5 seam (AC-625.3 cross-seam identity invariant).

C5_STATE_BUILD_FLAGS mirrors state_factory._STATE_BUILD_FLAGS so the bootstrap
can name the gating BUILD_STATE_* flag in operator errors before the lower
level StateEstimatorConfigError fires (AC-625.2). When the factory itself
rejects the configuration with the flag ON, the error wraps into
AirborneBootstrapError with __cause__ preserved (matches AZ-621 / AZ-622
patterns).

Constraints respected per AZ-618 umbrella: no per-component factory signature
changed; additive on top of AZ-619..AZ-623; no edits under state_factory,
pose_factory, or c5_state internals.

Tests: tests/unit/runtime_root/test_az625_c5_isam2_graph_handle_ordering.py
adds 8 tests covering AC-625.1..3 (presence + Protocol conformance, internal
key invariant, BUILD-flag-OFF error, unknown-strategy error, factory error
wrapping, cross-seam identity, wrapper short-circuit, wrapper fallback).
Autouse stubs added to test_az619/620/621/622/623 so prior phase tests stay
isolated from the new builder.

Quality gates: ruff format clean, ruff lint clean, 32/32 phase tests pass,
255/255 runtime_root + c5_state regression suite passes. Code review verdict
PASS (2 Low findings; full report in
_docs/03_implementation/reviews/batch_95_review.md).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 09:38:13 +03:00

8.3 KiB

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.