# Batch Report — Cycle 1 · Batch 3

**Tasks shipped**: AZ-270, AZ-272, AZ-279, AZ-281, AZ-283
**Date**: 2026-05-11
**Branch**: dev
**Review verdict**: PASS_WITH_WARNINGS — see `reviews/batch_03_review.md`

## Tasks

| ID | Title | Owner Layer | Outcome |
|----|-------|-------------|---------|
| AZ-270 | Composition Root | cross-cutting / `runtime_root.py` | Implemented — strategy registry, tier gating, topological-order construction, all-or-nothing teardown, `StrategyNotLinkedError` payload |
| AZ-272 | FdrRecord Schema | cross-cutting / `fdr_client/records.py` | Implemented — `orjson`-backed `serialise`/`parse`, forward-compat for unknown payload + top-level fields, overrun-payload contract |
| AZ-279 | WgsConverter | Layer 1 / `helpers/wgs_converter.py` | Implemented — `pyproj`-backed ECEF/ENU with hand-rolled OSM slippy-map tile math + `WgsConversionError` for shape / range / zoom guards |
| AZ-281 | EngineFilenameSchema | Layer 1 / `helpers/engine_filename_schema.py` | Implemented — strict `build`/`parse`/`matches_host` with anchored regex + enum validation |
| AZ-283 | DescriptorNormaliser | Layer 1 / `helpers/descriptor_normaliser.py` | Implemented — dtype-preserving single + batch L2 with zero-norm safety + `descriptor_metric()` source of truth |

## Files Changed

```
Modified:
  pyproject.toml                                            (+4 lines: pyproj, orjson pins)
  src/gps_denied_onboard/runtime_root.py                    (composition root impl)
  src/gps_denied_onboard/fdr_client/records.py              (full schema + serialiser)
  src/gps_denied_onboard/fdr_client/__init__.py             (re-exports)
  src/gps_denied_onboard/helpers/__init__.py                (re-exports)
  src/gps_denied_onboard/helpers/wgs_converter.py           (pyproj-backed converter)
  src/gps_denied_onboard/helpers/engine_filename_schema.py  (build/parse/matches_host)
  src/gps_denied_onboard/helpers/descriptor_normaliser.py   (L2 + zero-safe + dtype-preserving)
  src/gps_denied_onboard/_types/manifests.py                (EngineCacheKey, HostCapabilities)
  tests/unit/test_runtime_root_env_gate.py                  (updated to Config-typed compose_root)

Added:
  src/gps_denied_onboard/_types/geo.py                      (LatLonAlt, BoundingBox)
  tests/unit/test_az270_compose_root.py
  tests/unit/test_az272_fdr_record_schema.py
  tests/unit/test_az279_wgs_converter.py
  tests/unit/test_az281_engine_filename_schema.py
  tests/unit/test_az283_descriptor_normaliser.py
  _docs/03_implementation/reviews/batch_03_review.md
```

## Test Results

```
$ pytest tests/unit -q --timeout=30
203 passed, 2 skipped in 2.60s
```

Skips are environment-gated (cmake configure, actionlint), both verified
in CI.

### AC Coverage

| Task | ACs declared | ACs covered locally | Tier-2 deferred |
|------|--------------|---------------------|-----------------|
| AZ-270 | 6 + 2 NFR | 6 ACs + NFR-reliability (all-or-nothing) | NFR-perf (compose_root ≤ 750 ms on Jetson) |
| AZ-272 | 6 + 2 NFR | 6 ACs + NFR-reliability + invariant inline-blob | NFR-perf (serialise p99 ≤ 20 µs, parse p99 ≤ 50 µs on Jetson) |
| AZ-279 | 9 + 2 NFR | 9 ACs + determinism | NFR-perf (≤ 200 µs each on Jetson) |
| AZ-281 | 11 + 2 NFR | 11 ACs | NFR-perf (≤ 50 µs each on Jetson) |
| AZ-283 | 12 + 2 NFR | 12 ACs | NFR-perf-vector (≤ 50 µs / D=512) + NFR-perf-batch (≤ 5 ms / N=1000, D=512) |

Tier-2 perf budgets are owned by AZ-428..AZ-431; same deferral as
batch 2. The batch-3 modules expose stable APIs so those tasks plug in
without further code changes here.

## Dependency Pins

This batch amended `pyproject.toml` with two new runtime pins required
by AZ-272 and AZ-279 contracts:

- `pyproj>=3.6,<4.0` — WGS84 geodesy backend per AZ-279 contract
- `orjson>=3.9,<4.0` — FDR wire format per AZ-272 contract

Both names appear verbatim in the upstream contract documents and were
the named-backend constraint. AZ-263 left these unpinned; the pins are
added by the first batch that needs them — same pattern as batch 2.

## Adjacent Hygiene

This batch added four new DTOs to `_types/` because the AZ-279 / AZ-281
contracts explicitly import them from `_types`:

- `_types/geo.py` (new file): `LatLonAlt`, `BoundingBox`
- `_types/manifests.py`: `EngineCacheKey`, `HostCapabilities`

AZ-263 left these out; this batch closes the gap. No other helper or
component depends on them yet, so the additions are forward-compatible.

The `runtime_root.py` signature change (`compose_root()` → `compose_root(config)`)
forced an update to `tests/unit/test_runtime_root_env_gate.py` (now passes
`Config()` so the env-var fail-fast still fires). AC-8 of AZ-263 is
intact.

## Architecture Compliance

- `helpers/wgs_converter.py` imports only `numpy`, `pyproj`, `_types.geo`,
  and stdlib. AC-9 AST scan verifies no `components.*` imports.
- `helpers/engine_filename_schema.py` imports only `re`, `_types.manifests`,
  and stdlib. AC-11 AST scan verifies.
- `helpers/descriptor_normaliser.py` imports only `numpy` + stdlib.
  AC-12 AST scan verifies.
- `fdr_client/records.py` imports only `orjson` + stdlib + the
  `dataclasses` decorator. No upward imports.
- `runtime_root.py` imports from `gps_denied_onboard.config` only (the
  composition root is permitted to consume the config loader). The AC-6
  AST scan over every file under `components/` confirms no
  cross-component imports exist (the registry is the only sanctioned
  cross-component reference path).

No new circular imports.

## Review Findings Summary

Verdict: **PASS_WITH_WARNINGS**. Four Low-severity findings:

1. Two engine-cache types coexist (`EngineCacheEntry` vs `EngineCacheKey`)
   — recommend a docstring sentence on each.
2. NFR-perf microbenchmarks deferred to Tier-2 perf suite.
3. `pyproject.toml` dep amendment (pyproj + orjson) — by the consumer
   batch as designed.
4. AC-6 architecture lint lives in tests; future `importlinter` upgrade
   recommended once concrete components ship.

Full details in `reviews/batch_03_review.md`.

## Tracker Transitions

- AZ-270: To Do → In Progress → (batch close) In Testing
- AZ-272: To Do → In Progress → (batch close) In Testing
- AZ-279: To Do → In Progress → (batch close) In Testing
- AZ-281: To Do → In Progress → (batch close) In Testing
- AZ-283: To Do → In Progress → (batch close) In Testing
