mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-21 23:11:13 +00:00
362e93c626
Adds the C8 foundation: - FcAdapter / GcsAdapter / ReplaySink Protocols + contract DTOs in _types/fc.py (PortConfig, FcKind, FlightState, GpsStatus, Severity, TelemetryKind, FcTelemetryFrame, FlightStateSignal, GpsHealth, OperatorCommand, Subscription, Imu/Attitude samples). - Disjoint FcAdapterError / GcsAdapterError trees with SourceSetSwitchNotSupportedError <: SourceSetSwitchError per AC-9. - FcConfig + GcsConfig cross-cutting Config blocks with config-load validation (unknown strategy rejected at __post_init__). - runtime_root/fc_factory.py: build_fc_adapter / build_gcs_adapter with BUILD_FC_*/BUILD_GCS_* flag gating + INFO log on load + single-writer outbound-thread binding. - CovarianceProjector (helper, AZ-392): 6x6 -> 3x3 -> 2x2 -> sqrt(lambda_max) reduction; AP returns float m, iNav returns int mm with uint16 clamp + WARN + FDR record. Non-SPD / NaN / wrong-shape raise FcEmitError and emit an FDR ERROR record carrying frame_id. Contracts: - composition_root_protocol.md 1.1.0 -> 1.2.0 (added fc/gcs blocks + build_fc_adapter / build_gcs_adapter + outbound-thread binding). - fc_adapter_protocol.md unchanged (this batch implements v1.0.0). Tests: 410 pass / 2 skip / 0 fail (+53 new tests in batch 8). AZ-391 (inbound subscription) deferred to batch 9 — pulls YAMSPy as a new external dependency (iNav MSP2 decode). Co-authored-by: Cursor <cursoragent@cursor.com>
9.8 KiB
9.8 KiB
Batch 08 — Code Review
Batch: 8 of N Tasks: AZ-390 (FcAdapter / GcsAdapter Protocols + DTOs + factories + composition), AZ-392 (CovarianceProjector helper) Reviewer: autodev (7-phase) Verdict: PASS_WITH_INFO Date: 2026-05-11
Scope
| Task | Component / Concern | Files touched (prod) | Files touched (tests) |
|---|---|---|---|
| AZ-390 | C8 public Protocols + DTOs + errors + factories + Config blocks | _types/{fc.py,emitted.py,nav.py}, components/c8_fc_adapter/{__init__.py,interface.py,errors.py}, config/{schema.py,loader.py,__init__.py}, runtime_root/{__init__.py,fc_factory.py} |
tests/unit/c8_fc_adapter/{test_az390_adapter_protocol.py,test_smoke.py}, tests/unit/test_ac1_scaffold_layout.py |
| AZ-392 | C8 covariance projector | components/c8_fc_adapter/_covariance_projector.py |
tests/unit/c8_fc_adapter/test_az392_covariance_projector.py |
Phase 1 — AC compliance
| Task | ACs | Coverage |
|---|---|---|
| AZ-390 | 10 ACs (Protocol conformance, frozen-slot DTOs, enum membership, flag-OFF rejection, config-load rejection, single-writer thread, GCS factory, public-API gate, error hierarchy, INFO log) + NFR-perf | All passing in test_az390_adapter_protocol.py (36 tests). Public-API gate also covered by test_smoke.py::test_internal_modules_not_in_public_all. |
| AZ-392 | 7 ACs (reduction correctness, AP float m, iNav int mm, SPD violation, NaN guard, bit-stability, uint16 clamp) + NFR-perf |
All passing in test_az392_covariance_projector.py (17 tests). |
53 new tests added in batch; 410 total in suite (was 357), 2 pre-existing skips, 0 failures.
Phase 2 — Contract drift
composition_root_protocol.mdv1.1.0 → v1.2.0 (minor): added cross-cuttingfc: FcConfig+gcs: GcsConfigblocks onConfig, andbuild_fc_adapter/build_gcs_adapterfactories on the composition-root surface. Backwards-compatible — defaultFcConfig()+GcsConfig()preserve existing semantics; the four new env vars per block all have documented defaults.fc_adapter_protocol.mdis unchanged at v1.0.0 — this batch implements the v1.0.0 surface as specified.
Phase 3 — Architectural compliance
- No new dependencies. Every new module uses stdlib + numpy + pyyaml (all already pinned). YAMSPy entry is gated on AZ-391 (batch 9).
- Module-layout adherence:
_covariance_projector.pyis prefixed_permodule-layout.mdrule "internal helpers MUST start with_";c8_fc_adapter/__init__.__all__exposes ONLY the contract symbols (asserted bytest_smoke.test_internal_modules_not_in_public_all). - Layer 1 (helpers) discipline:
fc_factory.pyimports only fromgps_denied_onboard.config,gps_denied_onboard.logging, andgps_denied_onboard.components.c8_fc_adapter.{errors,interface}. No upward imports from c8 internals into the composition root. - ADR-002 build-time exclusion: the
_FC_BUILD_FLAGSmapping is the single source of truth tying a strategy slug to its build flag. The factory consultsos.environ(not config) because flag state is a build-time artifact per the ADR. - ADR-009 interface-first DI: all factories take
**depskeyword args; concrete strategies are registered viaregister_fc_adapter(slug, factory)from per-binary bootstrap modules, never imported directly by the composition root. - AC-NEW-3 (every payload class from t=0) preserved: this batch did NOT wire
take_offto callbuild_fc_adapter(the strict ordering is already enforced by batch 7'stake_off); only the factory itself landed. The wire-up happens when the first concrete adapter (AZ-393) lands. - Single-writer outbound thread (AC-6 / Invariant 8):
bind_outbound_emit_threadis process-global state guarded by a single lock; idempotent on same-thread re-binds (test-isolation friendly).
Phase 4 — Performance & reliability
- Factory build path is O(strategy-name-lookup): two dict lookups + one env-var read; AC-NFR sanity check verifies < 50 ms (typically < 100 µs measured locally).
- CovarianceProjector is O(1) per call: 2×2 closed-form eigenvalue, no LAPACK dispatch.
test_nfr_perf_projector_under_100us_per_callverifies < 100 µs avg over 1k iterations. - iNav clamp path emits at most one WARN + one FDR record per occurrence — no rate-limit needed because the clamp itself is a per-frame event already gated by the projector call rate (5 Hz emit per AC-1.4).
- SPD violation path is allocation-light: one numpy view (
cov_arr[:3, :3]), one 2×2 sub-view, two scalar reads. The FDR enqueue happens BEFORE the raise so the operator gets the post-flight forensic record even if the upstream caller swallows the exception.
Phase 5 — Test quality
- AC-1 Protocol tests use real stub classes (not
mock.MagicMock) soisinstance(x, FcAdapter)exercises the@runtime_checkableprotocol-fingerprint check honestly. - AC-2 DTO tests parametrise over EVERY contract DTO, asserting both frozen-instance immutability AND
__slots__presence. Forgettingslots=Trueon a new DTO fails the matching parametrise. - AC-3 enum tests assert exact member set (not a subset) — adding an unintended enum member also fails the gate.
- AC-4 / AC-5 tests separate the config-load gate from the build-time gate: unknown strategy fails at
FcConfig(...)construction; flag-OFF fails atbuild_fc_adapter(config); unregistered-but-known-strategy fails atbuild_fc_adapterwith a clear "registered strategies: [...]" message. - AC-6 cross-thread rebind test uses a real
threading.Thread— not just a differentthread_identargument — so the lock's cross-thread visibility is exercised. - AC-10 INFO-log tests pin
caplog.at_level(...)to the exact logger name so they don't accidentally pass on unrelated INFO records elsewhere in the process. - AZ-392 SPD-violation tests assert both the raise AND the FDR record carrying
reason+frame_id— a regression that drops the FDR record would not pass the raise-only test alone. - AZ-392 bit-stability test calls 20 times on the same input and asserts
len(set(results)) == 1— drift between calls (e.g. fromnumpy.eigvalsh-style round-off) fails the gate. - AZ-392 NFR-perf test uses
time.perf_counterwith 1 k iterations and a 100 µs/call budget — sufficient headroom for jittery CI runners while still catching an order-of-magnitude regression.
Arrange / Act / Assert pattern consistently applied in all new tests.
Phase 6 — Logging & FDR coverage
fc_factory.build_fc_adapter: INFO log per build (kind="c8.adapter.strategy_loaded",kv={strategy, port_device}).fc_factory.build_gcs_adapter: INFO log per build (kind="c8.gcs.strategy_loaded",kv={strategy, port_device}).CovarianceProjector: WARN log on iNav clamp (kind="c8.cov_projector.inav_clamped",kv={radius_mm_raw, clamped_to, frame_id}).CovarianceProjector: FDR ERROR record on every projection rejection (kind="log",payload.level="ERROR",payload.kind="c8.cov_projector.spd_violation",payload.kv={reason, frame_id, …}). Uses the existinglogrecord kind so no FDR schema bump is needed.- All log records follow the
kind+kvconvention required by AZ-266'sJsonFormatter.
Phase 7 — Security & risk surface
- RESTRICT-COMM-2 (iNav has no signing): enforced at config-load by
FcConfig.__post_init__— passingadapter="inav"with anysigning_key_source != "none"raisesConfigErrorwith the restriction code in the message. Covered bytest_ac5_inav_signing_key_combination_rejected. - Build-flag gate uppercases the env-var value (
os.environ.get(...).upper() == "OFF") soBUILD_FC_INAV=offis also honoured. The DEFAULT (no env var set) is ON-per-binary; concrete binaries SHOULD set it explicitly to keep the registry → flag mapping honest. - Strategy registry rejects duplicate registrations with different factories (
FcAdapterConfigError); same-factory re-registration is idempotent. This prevents a second bootstrap module from silently overriding the first. - No silent fallback: every config-load / build / projection failure raises or emits an FDR record. The covariance-projector FDR enqueue is best-effort but logs its own
c8.cov_projector.fdr_enqueue_failedso a silenced FDR doesn't hide the SPD failure. bind_outbound_emit_threaddoes not unbind on its own —clear_outbound_thread_binding()is intentionally scoped to test-isolation. A process that wants to re-bind in production must restart, which matches the single-writer invariant.
Informational findings (non-blocking)
take_offis not yet wired to callbuild_fc_adapter/build_gcs_adapter— this is the carry-over from batch 7's PASS_WITH_INFO finding #3. The wire-up will happen when the first concrete adapter lands (AZ-393), where thefc_factorycan be exercised end-to-end with a real adapter. Documented in the contract bump.SubscriptionProtocol has onlycancel()— the contract spec leaves open whether aSubscriptionshould also be a context manager. We did NOT add__enter__/__exit__because no AC requires it and AZ-391 hasn't started consuming the type yet. If AZ-391 needs ctx-manager semantics, that's a v1.1.0 contract bump onfc_adapter_protocol.md, not a regression._covariance_projector.pyis intentionally NOT inc8_fc_adapter/__init__.__all__— concrete adapters (AZ-393 / AZ-394) instantiate it via direct module import, since it is an internal helper. The smoke test asserts the public-API gate; no contract bump required because the projector is a concrete helper, not a Protocol.
Verdict
PASS_WITH_INFO — all ACs satisfied, all tests green, no architectural drift, one minor contract bump (composition_root_protocol.md 1.1.0 → 1.2.0) documented inline with migration notes. The three informational findings are forward actions tied to upcoming batches, not blockers.