Files
Oleksandr Bezdieniezhnykh 3acc7f33dd [AZ-270] [AZ-272] [AZ-279] [AZ-281] [AZ-283] Compose root + FDR schema + 3 Layer-1 helpers
AZ-270: composition root with strategy registry, tier-gated lookup,
topo-order construction, all-or-nothing teardown, StrategyNotLinkedError
payload.
AZ-272: orjson-backed FdrRecord serialise/parse with forward-compat for
unknown payload + top-level fields and canonical overrun-record shape.
AZ-279: pyproj-backed WGS84/ECEF/ENU + OSM slippy-map tile math with
WgsConversionError for shape/range/zoom guards.
AZ-281: strict EngineFilenameSchema build/parse/matches_host with
anchored regex + enum validation; round-trip identity by construction.
AZ-283: dtype-preserving (fp16/fp32) single + batch L2 normaliser with
zero-norm safety and descriptor_metric() source-of-truth.
pyproject.toml pins pyproj>=3.6 and orjson>=3.9 (named-backend deps per
the AZ-272 / AZ-279 contracts). New DTOs LatLonAlt + BoundingBox and
EngineCacheKey + HostCapabilities land in _types/ to back the helper
contracts.
203 unit tests pass (64 new). Review verdict: PASS_WITH_WARNINGS;
findings are perf-NFR deferrals + dep amendment + minor docstring polish.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 02:03:36 +03:00

17 KiB

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 deserializationorjson.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.