# Batch 99 — Cycle 2 — AZ-698 **Date**: 2026-05-20 **Tasks**: AZ-698 (Tlog trim + mid-flight alignment for replay). **Story points**: 5. **Jira status**: AZ-698 → `In Testing`. ## What shipped A normalised-cross-correlation aligner that finds the video's playback window inside a longer tlog, plus the plumbing to honor that window across the replay-mode composition root, replay coordinator, replay-side FC adapter, config schema, and CLI. - `find_aligned_window(tlog_path, video_path, config, ...) -> AlignedWindow` in `src/gps_denied_onboard/replay_input/auto_sync.py`. Returns `(tlog_start_ns, tlog_end_ns, offset_ms, confidence, used_fallback)`. - `AlignedWindow` DTO + `auto_trim` flag + `alignment_*` knobs on `ReplayConfig` / `ReplayAutoSyncConfig`. - `TlogReplayFcAdapter` skips messages with `_timestamp < tlog_start_ns` when seeded (`AC-3`). - `--auto-trim` CLI flag on `gps-denied-replay`, mutually exclusive with `--time-offset-ms`. ## Files changed Production (8): - `src/gps_denied_onboard/replay_input/interface.py` - `src/gps_denied_onboard/replay_input/auto_sync.py` - `src/gps_denied_onboard/replay_input/tlog_video_adapter.py` - `src/gps_denied_onboard/replay_input/__init__.py` (re-export `AlignedWindow`) - `src/gps_denied_onboard/components/c8_fc_adapter/tlog_replay_adapter.py` - `src/gps_denied_onboard/config/schema.py` - `src/gps_denied_onboard/config/loader.py` - `src/gps_denied_onboard/runtime_root/_replay_branch.py` - `src/gps_denied_onboard/cli/replay.py` Tests (1 new): - `tests/unit/replay_input/test_az698_window_alignment.py` Specs: - `_docs/02_tasks/done/AZ-698_tlog_trim_midflight_alignment.md` (moved from `todo/`, completion notes appended). ## AC coverage | AC | Test | Result | | ---- | -------------------------------------------------------------------------- | ------- | | AC-1 | `test_ac1_takeoff_aligned_offset_matches_az405_within_50ms` | PASS | | AC-2 | `test_ac2_mid_flight_alignment_locates_correct_window` | PASS | | AC-3 | `test_ac3_adapter_seek_skips_pre_window_messages`, `_default_no_seek_*` | PASS | | AC-4 | `test_ac4_validator_passes_for_takeoff_aligned_offset`, `_mid_flight_*` | PASS | | AC-5 | `test_ac5_cli_auto_trim_smoke_uses_find_aligned_window` | SKIPPED | AC-5 skip reason: the repo's `flight_derkachi.mp4` is a 134-byte placeholder, not a real recording. Live CLI smoke is covered by AZ-699 (validation runner) once the real video is sourced. ## Test run ``` tests/unit/replay_input/test_az698_window_alignment.py 12 PASS 1 SKIP tests/unit/replay_input/test_az405_auto_sync.py 14 PASS tests/unit/replay_input/test_az405_replay_input_adapter.py 13 PASS tests/unit/c8_fc_adapter/test_az399_tlog_replay_adapter.py 24 PASS 1 SKIP tests/unit/replay_input/test_tlog_ground_truth.py 12 PASS tests/unit/test_az697_gps_compare.py 10 PASS tests/unit/calibration/test_khp20s30_factory.py 9 PASS tests/unit/runtime_root/test_az687_pre_constructed_replay_mode.py 3 PASS tests/unit/test_az269_config_loader.py 9 PASS ``` Totals: **106 passed, 2 skipped, 0 failed.** No regressions in adjacent suites. ## Strict typing Baseline (pre-batch, by stash-and-rerun): 17 `mypy --strict` errors across 6 of the 8 touched `src/` files. After batch: 17 errors — same count, same kinds, with line numbers shifted only by added code. **Zero new strict-typing errors introduced by AZ-698.** Pre-existing errors are out-of-scope per `coderule.mdc` ("Pre-existing lint errors should only be fixed if they're in the modified area" — they were not in the bytes modified for AZ-698 ACs). The new public symbols (`find_aligned_window`, `AlignedWindow`, `_zero_mean_normalise`, `_resample_uniform`) carry explicit `npt.NDArray[np.float64]` annotations so they don't add to the debt. ## Code review verdict Inline self-review: code paths cover the spec's scope, fallback to head-takeoff on low confidence preserves AZ-405 behavior, adapter seek is opt-in via constructor kwarg so the `--skip-auto-sync` path is untouched. The normalised-cross-correlation switch is documented in the spec's "Implementation Notes" appendix as the algorithmic decision of record. ## Follow-up commit: multi-flight handling User-reported gap during the AZ-698 "In Testing" phase: real `derkachi.tlog` contains **three takeoffs**; the video covers only the last. The original AZ-698 happy path (`np.argmax`) and fallback (`detect_tlog_takeoff` on head) were both biased toward flight 1. Patched in a follow-up commit on top of batch 99: - New `_segment_flights_from_imu_energy` helper partitions the IMU energy stream into distinct flights using a motion-threshold + gap-tolerance walk. - `find_aligned_window` now restricts NCC search to the **last** detected segment. - Low-confidence fallback uses the last segment's start instead of re-running head-takeoff detection on the whole tlog. - `AlignedWindow` gains `flight_count_detected` + `selected_flight_index` for observability; both are surfaced in the `replay.auto_trim.resolved` / `…fallback_to_takeoff` log records. - New unit tests: segmenter happy paths (1-flight, 3-flight), ground-blip rejection, cruise-lull preservation; integration test proving `find_aligned_window` on a 3-flight tlog picks flight 3. Test totals after follow-up: **113 passed, 2 skipped, 0 failed.** Zero new mypy --strict errors (12 errors in scope, all pre-existing and unchanged). ## Next batch Batch 100 — **AZ-699** (real-flight validation runner). Depends on AZ-697 (ground truth) and AZ-698 (alignment) — both now in testing.