Files
gps-denied-onboard/.planning/phases/02-acceptance-criteria-test-taxonomy-observability-spine/02-07-SUMMARY.md
T
Yuzviak 7f76acfe29 docs(02-07): Phase 2 plan 07 SUMMARY + Phase 2 PHASE-SUMMARY
- 02-07-SUMMARY.md: log_schemas.py shape, test counts, AC-TRACEABILITY delta,
  E2E smoke notes, Phase 2 DoD checklist (9/10 checked, ROADMAP human action)
- 02-PHASE-SUMMARY.md: Phase 2 capstone — 236 tests, 15 ACs covered, 39 declared,
  10 hot-path structlog files, 3 boundary schemas, handoff notes for Phase 3
2026-05-11 18:53:43 +03:00

7.3 KiB

phase, plan, subsystem, tags, dependency_graph, tech_stack, key_files, decisions, metrics
phase plan subsystem tags dependency_graph tech_stack key_files decisions metrics
02-acceptance-criteria-test-taxonomy-observability-spine 07 observability
pydantic
boundary-log
structlog
unit-test
regression-gate
ac-traceability
requires provides affects
02-06-structlog-spine
02-05-ci-pipeline
02-04-ac-traceability
obs-boundary-schemas
ac-coverage-AC-1.4
ac-coverage-AC-4.3
ac-coverage-AC-2.1b
ac-coverage-AC-6.3
phase2-final-gate
Phase 3 AnchorVerifier wiring
Phase 5 MAVLink bridge wiring
Phase 6 REST middleware
added patterns
pydantic-v2-boundary-log-schemas
Literal-field-validation
ConfigDict-extra-forbid-frozen
model_dump-mode-json
created modified
src/gps_denied/obs/log_schemas.py
tests/test_log_schemas.py
src/gps_denied/obs/__init__.py
.planning/AC-TRACEABILITY.md
Boundary schemas use Literal not Enum — JSON-native, same validation speed, simpler migration path
capture_logs() skips processor chain by design; frame_id propagation verified via merge_contextvars directly
AC-2.1b remains DEFERRED (pending-phase-4) despite gaining 6 new test nodeids — schema contract tests do not satisfy the full AnchorVerifier integration requirement
duration completed tasks_completed files_created files_modified
~15 minutes 2026-05-11 4 2 2

Phase 2 Plan 07: Pydantic Boundary-Log Schemas + Final Regression Gate — Summary

One-liner: Pydantic v2 boundary-log schemas (MavlinkGpsInputEmitted, ApiRequestCompleted, AnchorDecision) with frozen+extra=forbid contracts, 20 unit tests, and Phase 2 final regression gate: 236 tests pass, AC --check exit 0.

What Was Built

1. log_schemas.py — 3 Pydantic v2 Boundary Models

File: src/gps_denied/obs/log_schemas.py

Model Fields AC Link Phase Wiring
MavlinkGpsInputEmitted lat_deg, lon_deg, alt_m, fix_type (ge=0 le=6), horiz_accuracy_m, source_label (Literal[3 values]), anchor_age_ms, cov_semi_major_m AC-4.3, AC-1.4, AC-1.3 Phase 5 / MAVOUT-01
ApiRequestCompleted path, method (Literal[5 values]), status_code (ge=100 lt=600), duration_ms AC-6.3 Phase 6 REST middleware
AnchorDecision decision (accept/reject), reason (AnchorRejectReason Literal[5]), n_inliers, mre_px VERIFY-02 Phase 3 AnchorVerifier

All models extend _BoundaryLogRecord(BaseModel) with ConfigDict(extra="forbid", frozen=True):

  • extra="forbid": producer-side field drift fails validation fast
  • frozen=True: records are immutable facts, not state

Type aliases exported: SourceLabel, AnchorRejectReason for producer code reuse.

2. tests/test_log_schemas.py — 20 Unit Tests, 17 AC-Tagged

Test Group Count AC Tags
MavlinkGpsInputEmitted round-trip + source_label 3 parametrize + rejects_unknown + fix_type 3 parametrize + extra_rejected + frozen 10 AC-4.3, AC-1.4
ApiRequestCompleted round-trip + unknown_method + status_bounds (2 assertions) 3 AC-6.3
AnchorDecision accept round-trip + 5 vocab parametrize + rejects_unknown_reason 7 AC-2.1b

Module-level: pytestmark = [pytest.mark.unit] Per-test: @pytest.mark.ac("AC-N.n") on relevant tests. All 20 tests pass in 0.04 seconds (no I/O, pure Pydantic validation).

3. AC-TRACEABILITY.md Delta

AC Before (tests) After (tests) Status
AC-1.4 2 7 OK
AC-4.3 20 26 OK
AC-2.1b 0 6 DEFERRED (pending-phase-4)*
AC-6.3 6 7 OK
ACs covered total 14 15

*AC-2.1b: schema contract tests added, but full AnchorVerifier integration requirement remains deferred to Phase 4.

4. End-to-End Frame-ID + Boundary-Record Smoke

structlog.contextvars.merge_contextvars (first processor in the configured chain) injects bound contextvars into every log record before the renderer fires. Verified directly:

merge_contextvars propagation OK: {'event': 'test', 'correlation_id': 999, 'source_label': 'satellite_anchored', 'fix_type': 3}
E2E spine + boundary-record smoke OK: {'event': 'mavlink_emit_gps_input', 'correlation_id': 999, 'source_label': 'satellite_anchored', 'fix_type': 3}

Note: structlog.testing.capture_logs() intentionally replaces the full processor chain with a minimal capture, so merge_contextvars does not run inside capture_logs() blocks. This is expected structlog behavior. The plan's Task 4 inline script was adapted to verify merge_contextvars directly rather than through capture_logs().

Phase 2 Definition of Done — verified by Plan 02-07

  • All 7 PLAN.md files have SUMMARY.md siblings (this one being the last)
  • pytest tests/ -q --ignore=tests/e2e passes at >= 216 (+ test_log_schemas.py count): 236 passed, 8 skipped
  • pytest -m unit / -m integration / -m blackbox each pass with zero failures (210 / 69 / 12)
  • scripts/gen_ac_traceability.py --check exit 0
  • git diff --exit-code .planning/AC-TRACEABILITY.md exit 0 (matrix committed clean)
  • from gps_denied.obs.logging_config import configure_logging works
  • from gps_denied.obs.log_schemas import MavlinkGpsInputEmitted, ApiRequestCompleted, AnchorDecision works
  • correlation_id=frame_id propagates from orchestrator binding via merge_contextvars to hot-path log calls
  • git diff --name-only tests/ shows only pytestmark + @pytest.mark.ac additions + new test_log_schemas.py (no logic edits)
  • ROADMAP.md Phase 2 row updated to "Plans Complete: 7/7" + "Status: Done" — REQUESTED FOR HUMAN REVIEWER after merge

ROADMAP.md Update Request

After merging Phase 2 branch into main:

  1. In ROADMAP.md, update Phase 2 row: Plans Complete → 7/7, Status → Done
  2. Optionally update the AC-2.1b row from "DEFERRED (pending-phase-4)" to add a note that schema contract tests now exist (6 nodeids), but the integration requirement is still pending Phase 4.

Deviations from Plan

Auto-adapted: capture_logs() assertion

Found during: Task 4 final regression gate Issue: The plan's Task 4 inline script asserts e["correlation_id"] == 999 inside a capture_logs() block. structlog.testing.capture_logs() replaces all processors with a bare capture — merge_contextvars does NOT run, so context vars are absent from captured events. This is documented structlog behavior, not a bug. Fix: Verified merge_contextvars propagation directly by calling the processor function against an event dict with bound context. The production chain (Plan 02-06's configure_logging) wires merge_contextvars as the first processor — the invariant holds in production. Impact: No code change needed; assertion strategy adapted in smoke test verification.

Self-Check

Files created:

  • src/gps_denied/obs/log_schemas.py — exists
  • tests/test_log_schemas.py — exists, 20 tests pass

Commits:

  • 94c1b76 — feat(02-07): add Pydantic v2 boundary-log schemas (OBS-01)
  • e87fb37 — test(02-07): add unit tests for boundary-log schemas (AC-02, OBS-01)
  • 14717c5 — chore(02-07): regenerate AC-TRACEABILITY.md with test_log_schemas nodeids

Gate results:

  • 236 total tests pass (baseline 216 + 20 new)
  • -m unit: 210 passed; -m integration: 69 passed; -m blackbox: 12 passed
  • AC --check exit 0; matrix diff clean

Self-Check: PASSED