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>
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.pyownership, 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 existingtests/unit/test_runtime_root_env_gate.pyfor the newcompose_root(config)signature - AZ-272 owns
src/gps_denied_onboard/fdr_client/records.py+src/gps_denied_onboard/fdr_client/__init__.pyre-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 addedEngineCacheKey+HostCapabilitiestosrc/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.tomlamended with two new pinned dependencies (pyprojfor AZ-279,orjsonfor 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.pyis 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_orderis 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
_composeat ~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_rootstub returned a hollowRuntimeRoot(binary, profile); replaced verbatim. The previousruntime_root.pyhad adataclassimport that is no longer needed at module level — confirmed it is still used (the new file imports it viadataclassforRuntimeRoot/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.loadsis the production decoder; type-validated before any field is used. YAML loading is in the AZ-269 path (out of scope here) and usesyaml.safe_load. _FILENAME_REis anchored with^...$; no ReDoS surface (the pattern is linear-time on input length).atomic_writeis 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/parsego throughorjson(C-extension); a single allocation per record. No O(n²) loops.WgsConverter._ECEF_FROM_LLAand_LLA_FROM_ECEFare module-level cachedpyproj.Transformerinstances; no per-call transformer setup cost.DescriptorNormaliser.l2_normalise_batchuses NumPy vectorisednp.linalg.norm(..., axis=1, keepdims=True); no Python-level row iteration._topo_orderis 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.recordsimportsorjsononly; no upward imports.helpers/wgs_converter.pyimportspyproj,numpy,_types.geoonly; no component imports.helpers/engine_filename_schema.pyimportsreand_types.manifestsonly.helpers/descriptor_normaliser.pyimportsnumpyonly.runtime_root.pyimportsconfig(allowed — it's the consumer) and nocomponents.*modules (because no concrete components exist yet; AC-6 enforces this going forward).- The
_close_partial_instancescleanup hook usesgetattr(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 fromcomponents.*— 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 bycompose_*. Tests reset it via theclear_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, carriesengine_path,content_hash,int8_calibration_path) andEngineCacheKey(AZ-281, the five-tuple filename key) sit side by side. They serve different purposes — aKeyis the parsed filename tuple, anEntryis the cache row that also tracks a content hash. C10's Manifest will need both. Worth a follow-up doc note in_types/manifests.pyso 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.0andorjson>=3.9,<4.0. Both are named-backend deps the upstream contract documents call out. Same justification as batch 2'sgtsam/atomicwritesamendment. - 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
importlinterwould 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.cfgso 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.