# Code Review Report

**Batch**: 3
**Tasks**: AZ-270 (Composition Root), AZ-272 (FdrRecord Schema), AZ-279 (WgsConverter), AZ-281 (EngineFilenameSchema), AZ-283 (DescriptorNormaliser)
**Date**: 2026-05-11
**Verdict**: PASS_WITH_WARNINGS

## Scope

Batch 3 closes the E-CC-CONF epic (AZ-270 ships the real composition root
with strategy registry and tier gates) and kicks off E-CC-FDR-CLIENT
(AZ-272 ships the wire-format schema). Three Layer-1 helpers — WGS
converter, engine-filename schema, descriptor normaliser — land in the
same batch because each is a small, contract-frozen unit that dozens of
downstream component tasks gate on.

## Phase 1: Context Loading

Read:

- `_docs/02_tasks/todo/AZ-270_compose_root.md` (6 ACs + 2 NFRs)
- `_docs/02_tasks/todo/AZ-272_fdr_record_schema.md` (6 ACs + 2 NFRs)
- `_docs/02_tasks/todo/AZ-279_wgs_converter.md` (9 ACs + 2 NFRs)
- `_docs/02_tasks/todo/AZ-281_engine_filename_schema.md` (11 ACs + 2 NFRs)
- `_docs/02_tasks/todo/AZ-283_descriptor_normaliser.md` (12 ACs + 2 NFRs)
- Five contracts under `_docs/02_document/contracts/`
- `_docs/02_document/module-layout.md` (`runtime_root.py` ownership, helper
  layer envelope, Layer-1 import rules)

Ownership envelopes resolved per `module-layout.md`:

- AZ-270 owns `src/gps_denied_onboard/runtime_root.py` (replaces the
  AZ-263 stub) + `tests/unit/test_az270_compose_root.py` + adjusts the
  existing `tests/unit/test_runtime_root_env_gate.py` for the new
  `compose_root(config)` signature
- AZ-272 owns `src/gps_denied_onboard/fdr_client/records.py` +
  `src/gps_denied_onboard/fdr_client/__init__.py` re-exports +
  `tests/unit/test_az272_fdr_record_schema.py`
- AZ-279 owns `src/gps_denied_onboard/helpers/wgs_converter.py` +
  `tests/unit/test_az279_wgs_converter.py`
- AZ-281 owns `src/gps_denied_onboard/helpers/engine_filename_schema.py` +
  `tests/unit/test_az281_engine_filename_schema.py`
- AZ-283 owns `src/gps_denied_onboard/helpers/descriptor_normaliser.py` +
  `tests/unit/test_az283_descriptor_normaliser.py`

Adjacent hygiene this batch (per `coderule.mdc` scope-discipline rule):

- New DTOs in `src/gps_denied_onboard/_types/geo.py` (`LatLonAlt`,
  `BoundingBox`) and added `EngineCacheKey` + `HostCapabilities` to
  `src/gps_denied_onboard/_types/manifests.py`. The AZ-279 and AZ-281
  contracts explicitly import these from `_types`; AZ-263 left them
  un-declared, so this batch closes the gap.
- `pyproject.toml` amended with two new pinned dependencies (`pyproj`
  for AZ-279, `orjson` for AZ-272). Both names appear verbatim in the
  upstream contract documents.

## Phase 2: Spec Compliance

### AZ-270 — Composition Root

| AC | Verification |
|----|--------------|
| AC-1 Default deployment composes | `test_ac1_default_deployment_composes` builds a 3-component graph and asserts every slot is populated |
| AC-2 Strategy/build-flag mismatch rejected | `test_ac2_strategy_not_linked_raises_with_payload` asserts `StrategyNotLinkedError(strategy_name, component_slug, available_strategies)` payload matches |
| AC-3 Operator-side excludes airborne | `test_ac3_operator_excludes_airborne_only` registers a strategy with `tier="airborne"`, asserts the same strategy in `compose_operator` raises |
| AC-4 Reachability proof | `test_ac4_runtime_root_smoke_exit_zero` runs `compose_root(Config())` with no component blocks and asserts a fully-formed `RuntimeRoot` |
| AC-5 Construction order respects deps | `test_ac5_construction_order_respects_dependencies` registers in reverse order, asserts the topo pass orders dependents after dependencies |
| AC-6 Single import point enforced | `test_ac6_only_compose_root_imports_concrete_strategies` AST-walks every file under `components/` and asserts no cross-component imports |
| NFR-reliability all-or-nothing | `test_nfr_reliability_partial_construction_closed_on_failure` injects a failing factory, asserts every prior `_Closable` had `close()` called |

The implementation provides a `_Registration` dataclass + global
`_STRATEGY_REGISTRY` keyed by `(component_slug, strategy_name)`, a
`register_strategy()` entrypoint (the only sanctioned write-path), a
`clear_strategy_registry()` helper for test isolation, a Kahn-style
topo-sort over the `depends_on` graph, and a `_close_partial_instances`
best-effort teardown hook.

### AZ-272 — FdrRecord Schema

| AC | Verification |
|----|--------------|
| AC-1 Every kind round-trips | `test_ac1_roundtrip_every_known_kind` parametrised over all 10 v1.0.0 kinds with kind-specific payload fixtures |
| AC-2 Forward-compat payload | `test_ac2_forward_compatible_unknown_payload_field_preserved` (and `_ac2b_unknown_top_level_field_preserved` for the top-level bucket) |
| AC-3 Unknown future kind opaque | `test_ac3_unknown_future_kind_returned_opaquely` |
| AC-4 Missing/non-int schema_version | `test_ac4_missing_schema_version_raises` + `test_ac4_non_integer_schema_version_raises` |
| AC-5 Overrun shape | `test_ac5_overrun_missing_dropped_count_rejected_on_parse` + `test_ac5_overrun_zero_dropped_count_rejected_on_serialise` |
| AC-6 Producer ID required | `test_ac6_empty_producer_id_rejected_on_serialise` |
| Invariant inline-blob cap | `test_nfr_oversized_inline_blob_rejected` |
| Pure determinism | `test_nfr_serialise_is_pure_byte_identical` |

Tier-2 perf NFR (`serialise` p99 ≤ 20 µs; `parse` p99 ≤ 50 µs) is
deferred to the Tier-2 perf suite — same pattern as batch-2 NFR-perf.

### AZ-279 — WgsConverter

| AC | Verification |
|----|--------------|
| AC-1 ECEF round-trip | `test_ac1_ecef_roundtrip` over 5 globally-distributed samples within `atol=1e-9` deg + `1e-6` m |
| AC-2 ENU 10 km round-trip | `test_ac2_enu_roundtrip_within_10_km` asserts horizontal residual < 1 m, vertical < 1 cm |
| AC-3 Slippy-map z18 round-trip | `test_ac3_slippy_map_tile_roundtrip_z18_contains_input` pinned to `(153295, 88392)` per OSM convention |
| AC-4 Lat range guard | `test_ac4_web_mercator_latitude_range_guard` |
| AC-5 Zoom range guard | `test_ac5_zoom_range_guard` |
| AC-6 Tile-xy range guard | `test_ac6_tile_xy_range_guard` |
| AC-7 ECEF shape contract | `test_ac7_ecef_shape_contract` |
| AC-8 Determinism | `test_ac8_determinism_byte_equal_outputs` (`tobytes()` equality) |
| AC-9 No upward imports | `test_ac9_no_upward_imports_to_components` (AST scan) |

Web-Mercator max-lat constant is `arctan(sinh(pi))` ≈ 85.0511287798066°
(matches the OSM-documented constant). `_enu_to_ecef_rotation`
implements the canonical local-tangent-plane basis at
`(lat, lon)`; ENU sign convention is `(east, north, up)`.

### AZ-281 — EngineFilenameSchema

| AC | Verification |
|----|--------------|
| AC-1 Reference example | `test_ac1_reference_example_builds_exact_string` |
| AC-2 Round-trip identity | `test_ac2_roundtrip_identity_over_10_random_tuples` (seeded `random.Random(2026)`) |
| AC-3 Host-match exact | `test_ac3_matches_host_exact_match` |
| AC-4 Host-mismatch no exception | `test_ac4_matches_host_tuple_mismatch_returns_false` (sm + trt variants) |
| AC-5 Precision enum strictness | `test_ac5_precision_enum_strictness` |
| AC-6 Model char set | `test_ac6_model_name_character_set_rejection` |
| AC-7 Reserved separator | `test_ac7_reserved_separator_collision_rejected` |
| AC-8 Version format | `test_ac8_three_segment_version_rejected` |
| AC-9 Parse malformed | `test_ac9_parse_rejects_malformed_filename` |
| AC-10 `.engine` suffix | `test_ac10_parse_requires_engine_suffix` |
| AC-11 No upward imports | `test_ac11_no_upward_imports_to_components` (AST scan) |

The implementation backs everything on a single anchored regex
`_FILENAME_RE` plus explicit `_validate_*` helpers for the producer
path. Round-trip identity holds by construction because `parse` extracts
the same five fields the regex consumed.

### AZ-283 — DescriptorNormaliser

| AC | Verification |
|----|--------------|
| AC-1 Unit-vector example | `test_ac1_unit_vector_example` |
| AC-2 Batch normalisation | `test_ac2_batch_normalisation` |
| AC-3 fp16 dtype preserved | `test_ac3_fp16_dtype_preservation` |
| AC-4 fp32 dtype preserved | `test_ac4_fp32_dtype_preservation` |
| AC-5 Zero-vector safe | `test_ac5_zero_vector_handling` + `_ac5b_zero_row_in_batch_remains_zero` |
| AC-6 fp32 idempotence | `test_ac6_idempotence_fp32` (`tobytes()` equality) |
| AC-7 fp16 idempotence | `test_ac7_idempotence_fp16_within_half_precision_tol` |
| AC-8 No in-place mutation | `test_ac8_no_in_place_mutation` |
| AC-9 Metric source-of-truth | `test_ac9_metric_is_inner_product_exact_string` |
| AC-10 fp64 rejected | `test_ac10_float64_dtype_rejected` |
| AC-11 Shape contract | `test_ac11_shape_contract_single_rejects_2d` + `_batch_rejects_1d` |
| AC-12 No upward imports | `test_ac12_no_upward_imports_to_components` (AST scan) |

The implementation routes through `float32` internally for norm
stability, then casts back to the caller dtype (no silent up-cast). The
batch path uses `np.where(norms == 0.0, 1.0, norms)` to avoid
division-by-zero without branching per row.

No Spec-Gap findings.

## Phase 3: Code Quality

- **SRP** — each module owns exactly one concern. `runtime_root.py` is
  marginally larger (registry + topo + compose + entrypoint) but every
  internal function has a sharp name; nothing leaks responsibility into
  generic "candidate"/"data" helpers.
- **Error handling** — every public surface raises one typed exception:
  `StrategyNotLinkedError`, `FdrSchemaError`, `WgsConversionError`,
  `EngineFilenameSchemaError`, `DescriptorNormaliserError`. Library
  errors (`orjson.JSONDecodeError`, `pyproj.ProjError`-tier exceptions)
  are wrapped at the public boundary.
- **Naming** — public symbols match the contract files verbatim.
  `RuntimeRoot.construction_order` is an additive field the contract
  permits but does not mandate; it's the observable used by AC-5 tests.
- **Complexity** — no function exceeds 50 lines; the busiest function is
  `_compose` at ~30 lines.
- **DRY** — shared helpers (`_enu_to_ecef_rotation`,
  `_validate_envelope_outgoing`, `_validate_overrun_payload`) are
  module-private; no duplication across helpers.
- **Test quality** — every AC has a directly-mapped test that asserts
  the contractually-named behaviour. AST-based import scans for the
  "no upward imports" invariant rather than string matches.
- **Dead code** — the previous AZ-263 `compose_root` stub returned a
  hollow `RuntimeRoot(binary, profile)`; replaced verbatim. The previous
  `runtime_root.py` had a `dataclass` import that is no longer needed at
  module level — confirmed it is still used (the new file imports it
  via `dataclass` for `RuntimeRoot`/`OperatorRoot`).

## Phase 4: Security Quick-Scan

- **No SQL string interpolation** anywhere in the batch.
- **No `shell=True` / `eval` / `exec`**.
- **No hardcoded secrets** — operator/airborne env-var lists are
  identifiers only.
- **No insecure deserialization** — `orjson.loads` is the production
  decoder; type-validated before any field is used. YAML loading is in
  the AZ-269 path (out of scope here) and uses `yaml.safe_load`.
- **`_FILENAME_RE` is anchored** with `^...$`; no ReDoS surface (the
  pattern is linear-time on input length).
- **`atomic_write` is delegated** to the AZ-280 helper (`Sha256Sidecar`);
  this batch does not introduce new disk-write code paths.

No security findings.

## Phase 5: Performance Scan

- `serialise` / `parse` go through `orjson` (C-extension); a single
  allocation per record. No O(n²) loops.
- `WgsConverter._ECEF_FROM_LLA` and `_LLA_FROM_ECEF` are module-level
  cached `pyproj.Transformer` instances; no per-call transformer setup
  cost.
- `DescriptorNormaliser.l2_normalise_batch` uses NumPy vectorised
  `np.linalg.norm(..., axis=1, keepdims=True)`; no Python-level row
  iteration.
- `_topo_order` is Kahn-style DFS at O(V+E) in the strategy graph
  (sub-msec for any realistic component count).

NFR microbenchmarks (AZ-272 serialise / parse latency; AZ-279 per-helper
latency; AZ-283 per-vector + batch latency) need Tier-2 hardware; same
deferral pattern as batch 2.

## Phase 6: Cross-Task Consistency

- `fdr_client.records` imports `orjson` only; no upward imports.
- `helpers/wgs_converter.py` imports `pyproj`, `numpy`, `_types.geo`
  only; no component imports.
- `helpers/engine_filename_schema.py` imports `re` and `_types.manifests`
  only.
- `helpers/descriptor_normaliser.py` imports `numpy` only.
- `runtime_root.py` imports `config` (allowed — it's the consumer) and
  no `components.*` modules (because no concrete components exist yet;
  AC-6 enforces this going forward).
- The `_close_partial_instances` cleanup hook uses `getattr(inst,
  "close", None)` so it does not require components to implement a
  particular interface — they opt in by exposing `.close()`.

No cross-task consistency findings.

## Phase 7: Architecture Compliance

Per `module-layout.md`:

- `helpers/*` (Layer 1): allowed imports are `_types`, stdlib, and named
  external deps (pyproj, numpy, orjson). PASS for AZ-279 / AZ-281 /
  AZ-283.
- `runtime_root.py` (composition root): allowed to import concrete
  strategies from `components.*` — but none exist yet, so this
  permission is unused.
- `fdr_client/records.py` (Layer 0 / cross-cutting): allowed imports are
  stdlib + `orjson`. PASS.
- The strategy registry is global state inside `runtime_root.py`. This
  is intentional and matches the ADR-009 "interface-first DI"
  prescription — the registry is filled by bootstrap modules, then
  consumed by `compose_*`. Tests reset it via the
  `clear_strategy_registry()` fixture.

No new circular imports. The import graph
`runtime_root → config → (stdlib + pyyaml)` and
`fdr_client.records → (stdlib + orjson)` are both acyclic.

No Architecture findings.

## Findings

| # | Severity | Category | File:Line | Title |
|---|----------|----------|-----------|-------|
| 1 | Low | Maintainability | _types/manifests.py | Two engine-cache types coexist (`EngineCacheEntry` and `EngineCacheKey`) |
| 2 | Low | Performance | tests/unit/test_az272*.py + test_az279*.py + test_az283*.py | NFR-perf microbenchmarks deferred to Tier-2 |
| 3 | Low | Scope | pyproject.toml | Batch 3 added `pyproj` + `orjson` deps that AZ-263 had not pinned |
| 4 | Low | Architecture | src/gps_denied_onboard/runtime_root.py + tests | AC-6 architecture lint relies on a tests-only AST scan |

### Finding Details

**F1: Two engine-cache types coexist** (Low / Maintainability)

- Location: `src/gps_denied_onboard/_types/manifests.py`
- Description: `EngineCacheEntry` (AZ-263 stub, carries `engine_path`,
  `content_hash`, `int8_calibration_path`) and `EngineCacheKey` (AZ-281,
  the five-tuple filename key) sit side by side. They serve different
  purposes — a `Key` is the parsed filename tuple, an `Entry` is the
  cache row that also tracks a content hash. C10's Manifest will need
  both. Worth a follow-up doc note in `_types/manifests.py` so the next
  reader doesn't think one supersedes the other.
- Suggestion: capture a short docstring sentence on each, calling out
  the other.
- Task: AZ-281

**F2: NFR-perf microbenchmarks deferred** (Low / Performance)

- Location: AZ-272 / AZ-279 / AZ-283 perf NFRs
- Description: Same pattern as batch 2. Tier-2 hardware-pinned budgets
  (serialise/parse latency, per-helper p99, batch p99) cannot be
  validated locally; AZ-428..AZ-431 own the Tier-2 perf suite.
- Suggestion: add corresponding `tests/perf/` files when AZ-428..AZ-431
  lands.
- Task: AZ-272, AZ-279, AZ-283

**F3: pyproject.toml dep amendment** (Low / Scope)

- Location: `pyproject.toml::dependencies`
- Description: Batch 3 added `pyproj>=3.6,<4.0` and `orjson>=3.9,<4.0`.
  Both are named-backend deps the upstream contract documents call out.
  Same justification as batch 2's `gtsam` / `atomicwrites` amendment.
- Suggestion: none — recorded so the AZ-263 implementation report and
  the Product-Implementation-Completeness audit reflect the batch-3 dep
  addition.
- Task: AZ-272, AZ-279

**F4: AC-6 architecture lint lives in tests** (Low / Architecture)

- Location: `tests/unit/test_az270_compose_root.py::test_ac6_only_compose_root_imports_concrete_strategies`
- Description: The "only compose_root imports concrete strategies"
  invariant is enforced by an AST scan inside the unit suite. A future
  CI lane that runs `importlinter` would catch the same thing earlier.
  For v1.0.0 the unit-test gate is sufficient (and necessary — without
  any concrete strategies yet, there's no need for a separate tool).
- Suggestion: when the first concrete component strategy is wired in,
  consider adding an `importlinter.cfg` so the check runs at lint time
  too.
- Task: AZ-270

## Verdict

**PASS_WITH_WARNINGS**. Four Low-severity findings, all informational
follow-ups (per-NFR deferrals, dep amendment, docstring polish, and a
future lint-tier upgrade). Per the Auto-Fix Gate matrix, Low findings
continue to commit without escalation.

## Test Run Summary

- **Local**: 203 passed, 2 skipped (cmake configure, actionlint — both
  CI-gated). Includes 64 new batch-3 tests.
- **Coverage**: every AC across all five tasks has at least one
  corresponding test. NFR-perf budgets are deferred to Tier-2.
