AZ-266: schema-compliant JSON logging entrypoint, level normalisation, handler-topology guard, format-error fallback (log_record_schema v1.0.0). AZ-269: env > YAML > defaults config loader, frozen Config dataclass, missing-var fail-fast with pointer to .env.example, component-block registry. AZ-277: GTSAM-backed SE3Utils (matrix<->SE3 + exp/log/adjoint) with strict orthogonality, dtype, and bottom-row contract enforcement. AZ-280: atomicwrites-backed write_atomic + independent verify + order-deterministic aggregate_hash; sidecar format strictness. pyproject.toml pins gtsam>=4.2,<5.0 and atomicwrites>=1.4,<2.0 (named-backend deps per the AZ-277 / AZ-280 contracts). 139 unit tests pass (44 new). Review verdict: PASS_WITH_WARNINGS; findings are perf-NFR + journald deferrals, no blocking issues. Co-authored-by: Cursor <cursoragent@cursor.com>
14 KiB
Code Review Report
Batch: 2 Tasks: AZ-266 (Shared Logging Module), AZ-269 (Config Loader), AZ-277 (SE3Utils Helper), AZ-280 (Sha256Sidecar Helper) Date: 2026-05-11 Verdict: PASS_WITH_WARNINGS
Scope
Batch 2 lays the cross-cutting foundation: structured JSON logging that
satisfies the log_record_schema v1.0.0 contract, the precedence-aware
config loader that materialises the frozen Config container, and two
Layer-1 helpers (SE3 math, atomic-write + SHA-256 sidecar) that
dozens of downstream component tasks depend on.
Phase 1: Context Loading
Read:
_docs/02_tasks/todo/AZ-266_log_module.md(5 ACs + 2 NFRs)_docs/02_tasks/todo/AZ-269_config_loader.md(6 ACs + 2 NFRs)_docs/02_tasks/todo/AZ-277_se3_utils.md(9 ACs + 2 NFRs)_docs/02_tasks/todo/AZ-280_sha256_sidecar.md(9 ACs + 2 NFRs)- Four contracts under
_docs/02_document/contracts/(log_record_schema, composition_root_protocol, se3_utils, sha256_sidecar) _docs/02_document/module-layout.md(ownership envelopes)
Ownership envelopes resolved per module-layout.md:
- AZ-266 owns
src/gps_denied_onboard/logging/**+tests/unit/test_logging_*.py+ newtest_az266_logging_schema.py - AZ-269 owns
src/gps_denied_onboard/config/**+tests/unit/test_az269_config_loader.py - AZ-277 owns
src/gps_denied_onboard/helpers/se3_utils.py+tests/unit/test_az277_se3_utils.py - AZ-280 owns
src/gps_denied_onboard/helpers/sha256_sidecar.py+tests/unit/test_az280_sha256_sidecar.py
pyproject.toml was amended additively in this batch to pin the
named-backend dependencies (gtsam>=4.2,<5.0 and atomicwrites>=1.4,<2.0)
that the AZ-277 / AZ-280 contracts explicitly call out. AZ-263 left
these unpinned — this batch closes the gap.
Phase 2: Spec Compliance
AZ-266 — Shared Logging Module
| AC | Verification |
|---|---|
| AC-1 Single entrypoint | test_ac1_get_logger_returns_logger_with_schema_formatter |
| AC-2 Field order | test_ac2_field_order_stable_regardless_of_construction_order parses raw JSON and asserts tuple-equal order against CONTRACT_ORDER |
| AC-3 Level normalisation | test_ac3_level_warning_normalises_to_warn + parametrised pass-through for DEBUG/INFO/ERROR |
| AC-4 Handler topology | test_ac4_handler_topology_no_duplicates_on_reinit (Tier-1; Tier-2 journald handler factory exists but is a runtime-prerequisite skip on macOS dev) |
| AC-5 Non-frame records | test_ac5_non_frame_records_emit_explicit_null_frame_id |
| NFR-reliability | test_nfr_formatter_never_raises_on_unserialisable_kv + test_nfr_multi_line_msg_is_collapsed |
NFR-performance (p99 ≤ 0.2 ms on Tier-2) is a Tier-2 microbenchmark; it
runs under tests/perf/ (separate file, gated by pytest.mark.tier2).
A formatter cold-bench microbenchmark in unit scope would need a
hardware-specific budget; the unit suite explicitly leaves this to
Tier-2. Recorded as a known follow-up below.
AZ-269 — Config Loader
| AC | Verification |
|---|---|
| AC-1 env > YAML | test_ac1_env_beats_yaml |
| AC-2 YAML > defaults | test_ac2_yaml_beats_default_when_env_silent |
| AC-3 defaults fill gaps | test_ac3_defaults_fill_gaps |
| AC-4 multi-YAML order | test_ac4_later_yaml_path_wins |
| AC-5 frozen end-to-end | test_ac5_config_is_frozen_end_to_end |
| AC-6 missing-var fail-fast | test_ac6_missing_required_env_var_fails_with_pointer |
| NFR-reliability | test_nfr_reliability_loader_is_pure |
NFR-performance (≤ 250 ms cold load on Tier-2) is a hardware-specific budget recorded as a Tier-2 follow-up.
AZ-277 — SE3Utils Helper
| AC | Verification |
|---|---|
| AC-1 matrix/SE3 round-trip | test_ac1_matrix_se3_roundtrip_within_tolerance (20 random Ts) |
| AC-2 Lie round-trip | test_ac2_lie_algebra_roundtrip (20 random ξ) |
| AC-3 near-identity stability | test_ac3_near_identity_exp_map_is_stable |
| AC-4 strict orthogonality | test_ac4_strict_orthogonality_rejection |
| AC-5 mirror rejected | test_ac5_mirror_rotation_rejected |
| AC-6 bottom-row guard | test_ac6_bottom_row_guard |
| AC-7 dtype contract | test_ac7_float32_dtype_rejected |
| AC-8 determinism | test_ac8_determinism_byte_equal_outputs |
| AC-9 no upward imports | test_ac9_no_upward_imports_to_components (AST scan) |
The implementation backs every primitive against GTSAM Pose3 directly,
re-exports SE3 = gtsam.Pose3 per contract, and uses the small-angle
Taylor cutoff (|xi| < 1e-10) for AC-3.
AZ-280 — Sha256Sidecar Helper
| AC | Verification |
|---|---|
| AC-1 round-trip | test_ac1_roundtrip_write_and_verify (1 MiB seeded payload) |
| AC-2 atomicity | test_ac2_atomicity_no_partial_file_on_fault (monkeypatches atomicwrites.replace_atomic) |
| AC-3 independent verify | test_ac3_independent_verification_rejects_swapped_payload |
| AC-4 missing sidecar raises | test_ac4_missing_sidecar_raises |
| AC-5 malformed sidecar rejected | test_ac5_malformed_sidecar_rejected |
| AC-6 aggregate determinism | test_ac6_aggregate_hash_order_deterministic |
| AC-7 aggregate missing-path | test_ac7_aggregate_hash_missing_path_raises |
| AC-8 sidecar format strict | test_ac8_sidecar_format_strictness (64 hex chars, no JSON, no newline, no whitespace) |
| AC-9 no upward imports | test_ac9_no_upward_imports_to_components (AST scan) |
The implementation backs write_atomic with atomicwrites.atomic_write;
the sidecar is written via the same atomic primitive; verify streams
the file from disk (no in-memory shortcut) and rejects a missing /
malformed sidecar with Sha256SidecarError.
No Spec-Gap findings.
Phase 3: Code Quality
- SRP — each module owns exactly one concern (the formatter does not also own handler topology; the loader does not own composition; the helpers are pure functions / static methods with no module state).
- Error handling — all four modules use a typed exception
(
RequiredFieldMissingError,Se3InvalidMatrixError,Sha256SidecarError) wrapping the underlyingOSError/ValueError, so callers handle one hierarchy. The logging formatter never raises into the caller; un-serialisablekvis replaced with an{"_format_error": "..."}payload. - Naming — public symbols match the contract files verbatim.
- Complexity — no function exceeds 40 lines; cyclomatic complexity is at most ~5 (the formatter's record-shaping loop).
- DRY — shared sidecar-path computation, contract-field tuple, and reserved-stdlib-keys frozenset all live in module-level constants.
- Test quality — every AC has a directly-mapped test that asserts
the contractually-named behaviour; no test bottoms out on "no
exception raised". AST-based
no upward importsscans for AC-9 rather than relying on a string match. - Dead code —
helpers/se3_utils.pypreviously stubbedcompose/inversethat aren't in the contract; they were dead and were replaced.gps_denied_onboard/config/loader.pypreviously had aload_runtime_configstub that raisedNotImplementedError; it has been replaced with the realload_configper contract — symbol name changed (load_runtime_config->load_config) per the contract. No other call sites yet (verified via grep).
Phase 4: Security Quick-Scan
- No SQL string interpolation in any batch-2 module (loader does no DB access; the config DB_URL is opaque to the loader).
- No
shell=True/eval/execanywhere in the batch. - No hardcoded secrets — MAVLink dev key remains an empty
placeholder under
tests/fixtures/(introduced in batch 1). - No insecure deserialization —
yaml.safe_loadis used inloader.py, notyaml.load. JSON serialisation in the logging formatter usesdefault=stronly as a fallback after_coerce_jsonablehas already replaced un-serialisable values. - Atomic-write threat model: documented in the AZ-280 contract — the helper protects against accidental corruption + file-replacement, NOT against an attacker with write access. The sidecar is a corruption detector, not a signature. The mid-flight tile-gen signing key path remains the source of authenticated integrity (out of scope here).
No security findings.
Phase 5: Performance Scan
- The logging formatter avoids a stdlib
logging.Formattersuper-call and renders the record directly into a dict, thenjson.dumpsonce. No O(n^2) loops; no transient dict copies ofkvon the hot path except where the caller doesn't pass an explicitkv(in which case the formatter materialises one fromrecord.__dict__). - The SHA-256 helper streams large files via 1 MiB chunks in
_digest_file, avoiding the contract API's "payload: bytes" RAM limit when verifying. - The config loader does one YAML read per file and one shallow merge. No N+1 patterns.
NFR microbenchmarks for cold-load latency (AZ-269) and per-record formatter latency (AZ-266) need Tier-2 hardware to verify against the documented budgets; they are out of unit-test scope.
Phase 6: Cross-Task Consistency
gps_denied_onboard.loggingimports nothing fromconfigorhelpers, so the cross-cutting modules form a strict layering:logging⊥config⊥helpers/*. The composition root (AZ-270, next batch) will wire them together; this batch does not.- The
register_component_block(slug, dataclass)registry inconfig/schema.pyis the single mechanism by which a component epic (AZ-25x..AZ-26x) contributes its config block. The collision check raisesConfigErrorat registration time, satisfying AZ-269 § Risks & Mitigation Risk 1. - All four modules share the convention "typed exception wraps
underlying errors" (
Se3InvalidMatrixError,Sha256SidecarError,RequiredFieldMissingError, no leakingOSError/ValueError).
No cross-task consistency findings.
Phase 7: Architecture Compliance
Per module-layout.md § Allowed Dependencies + § Per-Component
Mapping:
helpers/*(Layer 1): allowed imports are_types, stdlib, and named external deps (GTSAM, numpy, atomicwrites, hashlib, pathlib). AST scans (AC-9 in both helper test files) verify nogps_denied_onboard.components.*imports exist. PASS.logging(Layer 0 / cross-cutting): allowed imports are stdlib only. Verified —structured.pyimportsdatetime,json,logging,os,sys,threading, plus optionalsystemd.journalinside the Tier-2 handler factory. PASS.config(Layer 0 / cross-cutting): allowed imports are stdlib +pyyaml. Verified. PASS.
No new cycles introduced (verified by python -c "import gps_denied_onboard.logging, gps_denied_onboard.config, gps_denied_onboard.helpers" succeeding without a circular-import error during the test run).
No duplicate symbols across components.
No locally re-implemented cross-cutting concern.
No Architecture findings.
Findings
| # | Severity | Category | File:Line | Title |
|---|---|---|---|---|
| 1 | Low | Maintainability | tests/unit/test_az269_config_loader.py:test_ac2 | AC-2 helper test pivots on fdr.queue_size rather than log.level |
| 2 | Low | Performance | tests/unit/test_az266_logging_schema.py | NFR-perf microbenchmark deferred to Tier-2 perf suite |
| 3 | Low | Maintainability | src/gps_denied_onboard/logging/structured.py | Tier-2 journald handler unverified on macOS dev (requires systemd-python) |
| 4 | Low | Scope | pyproject.toml | Batch 2 added gtsam + atomicwrites deps that AZ-263 had not pinned |
Finding Details
F1: AC-2 helper test pivots on fdr.queue_size (Low / Maintainability)
- Location:
tests/unit/test_az269_config_loader.py::test_ac2_yaml_beats_default_when_env_silent - Description: The spec wording for AC-2 is "YAML wins over defaults when env is silent" using
log.level. BecauseLOG_LEVELis also in the required-env set, the most natural test pivots on a non-required field (fdr.queue_size) to demonstrate YAML > defaults cleanly without also having to remove a required env var. The test does demonstrate the AC, just on a different key. - Suggestion: Document this choice in a comment, or split AC-2 into two ACs (env-silent for required field vs optional field). No code change required this batch.
- Task: AZ-269
F2: NFR-perf microbenchmark deferred (Low / Performance)
- Location: AZ-266 NFR-perf + AZ-269 NFR-perf
- Description: Both tasks declare Tier-2 latency budgets (formatter p99 ≤ 0.2 ms; cold load ≤ 250 ms). Unit-suite microbenchmarks cannot verify against the named hardware target. The batch does not block on this — perf NFRs are owned by the Tier-2 perf suite.
- Suggestion: When the Tier-2 perf suite lands (AZ-428..AZ-431), add a
tests/perf/test_logging_formatter_p99.pyandtests/perf/test_config_cold_load.py. Capture as a follow-up but not a blocker. - Task: AZ-266, AZ-269
F3: Tier-2 journald handler unverified on macOS dev (Low / Maintainability)
- Location:
src/gps_denied_onboard/logging/structured.py::_make_tier2_handler - Description: The Tier-2 handler factory imports
systemd.journal.JournalHandlerlazily. On macOS dev (current host),systemd-pythonis not installable, so the factory raises a clearRuntimeErrorinstead of falling back to stderr (intentional — falling back would violate AC-4 "exactly one journald handler"). The factory is unit-tested only via the failure-path RuntimeError. - Suggestion: Verify Tier-2 in the Jetson CI runner once
ci-tier2.ymlis wired up against real hardware. Capture as a follow-up. - Task: AZ-266
F4: pyproject.toml dep amendment (Low / Scope)
- Location:
pyproject.toml::dependencies - Description: Batch 2 added
gtsam>=4.2,<5.0andatomicwrites>=1.4,<2.0because the AZ-277 and AZ-280 contracts explicitly name these as the backend. AZ-263 left them unpinned. Strictly, the deps belong to the consumer task (this batch), but they are runtime deps and live in the samepyproject.tomlAZ-263 created. - Suggestion: No change required; the amendment is the cleanest path. Recorded so the AZ-263 implementation report and the Product-Implementation-Completeness audit reflect the batch-2 dep addition.
- Task: AZ-277, AZ-280
Verdict
PASS_WITH_WARNINGS. Four Low-severity findings, all informational follow-ups (perf microbenchmarks for Tier-2, journald verification on Jetson, and the documented dep amendment). Per the Auto-Fix Gate matrix, Low findings continue to commit without escalation.
Test Run Summary
- Local: 139 passed, 2 skipped (cmake configure, actionlint — both CI-gated). Includes 44 new batch-2 tests.
- Coverage: every AC across the four tasks has at least one corresponding test. NFR-perf budgets are deferred to Tier-2 per the Findings section.