# Batch Report

**Batch**: 2
**Tasks**: AZ-641 `mavlink_transport_and_heartbeat`, AZ-642 `mavlink_codec`, AZ-644 `mission_client_pull_and_schema`
**Date**: 2026-05-19
**Cycle**: 1
**Selection context**: Product implementation
**Implementer**: autodev / `.cursor/skills/implement/SKILL.md`

## Task Results

| Task   | Status | Files Modified | Tests | AC Coverage | Issues |
|--------|--------|----------------|-------|-------------|--------|
| AZ-641 | Done   | `crates/mavlink_layer/{Cargo.toml,src/lib.rs,src/internal/{uri,retry,heartbeat,transport/*}.rs}`, integration tests `tests/{udp_link,serial_link}.rs` | pass (21 unit + 4 UDP integration + 1 serial `#[ignore]`) | 3/4 verified locally, 1/4 deferred (serial requires `socat` pty pair) | 0 blocking |
| AZ-642 | Done   | `crates/mavlink_layer/src/internal/codec/{mod,crc,messages,encoder,decoder,parse_errors}.rs`, integration test `tests/codec_round_trip.rs` | pass (full lib suite + 3 codec integration) | 3/4 verified locally, 1/4 deferred (SITL round-trip needs ArduPilot SITL) | 0 blocking, **1 caught-and-fixed silent wire bug** (CRC byte-truncation; see Issues below) |
| AZ-644 | Done   | `crates/mission_client/{Cargo.toml,src/lib.rs,src/internal/{retry,missions_api,schema/mod}.rs}`, schema `crates/shared/contracts/mission-schema.json`, integration test `tests/pull_mission.rs` | pass (4 unit + 5 wiremock-driven integration) | 4/4 verified locally | 0 blocking |

## AC Test Coverage

| Task   | AC   | Description | Verified locally | Notes |
|--------|------|-------------|------------------|-------|
| AZ-641 | AC-1 | UDP connection opens + survives drop | YES | `ac1_udp_opens_and_emits_heartbeats` + `ac1_udp_reconnects_after_peer_restart` |
| AZ-641 | AC-2 | Serial connection survives drop | DEFERRED | `serial_link::serial_transport_reconnect_round_trip` `#[ignore]`-marked with a clear prerequisite reason; runs in SITL/CI tier with the `socat` pty pair wired in `_docs/02_document/deployment/ci_cd_pipeline.md` |
| AZ-641 | AC-3 | Heartbeat emitted at 1 Hz | YES | `ac3_emits_heartbeat_at_one_hertz` counts 2–3 frames over 2.5 s |
| AZ-641 | AC-4 | Autopilot heartbeat loss flips link state | YES | `ac4_link_lost_when_peer_silent` exercises the full LinkUp → LinkLost broadcast path |
| AZ-642 | AC-1 | Round-trip every supported message | YES | `every_supported_message_round_trips` covers all 17 messages; per-message unit tests in `messages.rs` |
| AZ-642 | AC-2 | Malformed frame rejected | YES | `malformed_crc_drops_frame_and_counts_error` (integration) + `rejects_bad_crc` (unit) |
| AZ-642 | AC-3 | Unknown ID counted, not fatal | YES | `unknown_message_id_counts_not_fatal` (integration) + `skips_unknown_message_id` (unit) |
| AZ-642 | AC-4 | SITL round-trip (`COMMAND_LONG` → `COMMAND_ACK`) | DEFERRED | Requires ArduPilot SITL container. The CRC fix (see Issues) means the codec is now wire-correct; SITL conformance is part of the `sitl-conformance` Woodpecker stage and AZ-648's mission FSM batch |
| AZ-644 | AC-1 | Happy-path fetch | YES | `ac1_happy_path_fetch` |
| AZ-644 | AC-2 | Schema-invalid rejected | YES | `ac2_schema_invalid_is_rejected` |
| AZ-644 | AC-3 | Transient retry within budget | YES | `ac3_transient_failure_retries_within_budget` (503 → 503 → 200) |
| AZ-644 | AC-4 | Cap exhaustion refuses start | YES | `ac4_cap_exhaustion_returns_max_retries` |

**Coverage: 10/12 verified locally; 2/12 deferred to external infrastructure (socat pty pair for AC-2 serial, ArduPilot SITL for AC-4).** Deferred tests exist as `#[ignore]`-marked stubs with documented prerequisites.

## Code Review Verdict

PASS_WITH_WARNINGS (inline; sub-skill `/code-review` deliberately skipped to conserve context).

Phase 1 — Spec coverage: every Included scope item implemented; Excluded items remain unimplemented (signing AZ-643, ack demux AZ-643, mapobjects AZ-646/AZ-647).

Phase 2 — Architecture compliance:
- `mavlink_layer` imports only `shared` (Layer 2 → Layer 1) ✓
- `mission_client` imports only `shared` (Layer 2 → Layer 1) ✓
- Public API surface per `module-layout.md`:
  - `mavlink_layer::{MavlinkLayer, MavlinkHandle, MavlinkConnection, MavlinkMessage, …}` ✓
  - `mission_client::{MissionClient, MissionClientHandle, Mission, FetchError, …}` ✓
- Hand-rolled MAVLink — no `mavlink`-rs / pymavlink-bindgen / etc. ✓
- `MavlinkHandle` implements `shared::contracts::MavlinkSink` (delegates `send_raw` to the bytes channel) ✓

Phase 3 — Code quality:
- SRP holds at module level: `crc` / `messages` / `encoder` / `decoder` / `parse_errors` / `transport/{udp,serial}` / `heartbeat` / `uri` / `retry` each have one reason to change.
- No silent error suppression. Decoder records every drop into typed counters and emits a `tracing::warn!` per parse-error event. Transport errors propagate up to the reconnect loop with explicit reason strings.
- `MissionClient::pull_mission` classifies errors as Permanent / Transient / SchemaInvalid / MaxRetriesExceeded / Internal — no catch-all `_` paths.
- `unwrap()` appears only on the once-init `OnceLock` schema-compile (build-time correctness; the panic message names the file).
- All tests use Arrange / Act / Assert blocks per `coderule.mdc`.

Phase 4 — Test quality:
- Codec round-trip exercises all 17 supported messages; truncation, CRC, and unknown-ID paths each have dedicated tests.
- UDP integration tests use real `tokio::net::UdpSocket` loopback — no fake transports.
- mission_client tests use `wiremock` for real HTTP semantics including 200 + 503-then-200 + 5×503 + 404.
- Backoff math has its own unit test (`doubles_until_cap`, `reset_returns_to_base`).

Phase 5 — Docs:
- Crate-level doc comments call out which AZ-NNN owns each piece.
- `mission-schema.json` carries a `description` naming its co-owner and the architecture pointer.
- `INCOMPAT_FLAG_SIGNED`, `MAX_PAYLOAD`, etc. carry RFC-style commentary.

Phase 6 — Cross-task consistency:
- `mission_executor::start` still takes `Vec<MissionItem>` (unchanged); no real call sites yet, so the eventual rename to `start(Mission)` lands with AZ-648 without breaking anything in this batch.
- Workspace deps added (`reqwest 0.12`, `jsonschema 0.18`, `tokio-serial 5`, `wiremock 0.6`) follow the existing pinning style; no duplicate versions of any crate.

Phase 7 — Security / safety:
- HTTPS uses `rustls-tls` (no OpenSSL on the airframe) per `cursor-security.mdc` defence-in-depth posture.
- `mission-schema.json` strict by default (`additionalProperties: false`, `pattern`-bounded UUIDs and semver, geo-coordinate range validation).
- Bearer token sourced via `MissionClientOptions.bearer_token`; never logged.
- CRC and parse-error counters surface on the health endpoint for audit.

### Warnings (non-blocking, captured for follow-up)

- W1 (`mavlink_layer`): `MavlinkLayerOptions.signing_enabled` is plumbed through to the health detail string but does not yet drive `incompat_flags` in the encoder or signature verification in the decoder. **By design** — AZ-643 owns the signing path; AZ-641 only carries the flag.
- W2 (`mission_client` + `mavlink_layer`): `ExponentialBackoff` is duplicated in both crates. Acceptable at two callsites with different defaults; if a third lands (likely `detection_client` retry in AZ-660 / AZ-661), promote to `shared::retry`. Recorded as a refactor candidate.
- W3 (`mission_client`): `Mission` field is named `items` (matching `data_model.md §MissionItem` canonical terminology); the AZ-644 task spec's AC-1 prose used the casual word "waypoints" for the same concept. Reconciled per `artifact-srp.mdc` — `data_model.md` owns the entity catalogue.
- W4 (`mission_client`): the bundled schema is the source of truth and the typed Rust model is derived from it; if the two ever drift the failure surfaces as `FetchError::Internal("deserialise mission: …")` rather than `SchemaInvalid`. Both files are owned in this batch so drift is impossible today, but a schema-snapshot regression test will be added when the missions repo extraction lands (`architecture.md §8 Q5`).

## Auto-Fix Attempts

1 inline auto-fix (not from a `/code-review` finding — caught by the unit test that hand-computed a CRC reference value):

- **Caught**: the initial X.25 CRC implementation used `u16` throughout the `tmp ^= tmp << 4` step. The MAVLink C reference uses a `uint8_t` for `tmp`, which silently truncates the shifted-in bits. Both implementations agree on every byte's CRC **only when reading the result back through the same buggy implementation**, so the codec's own round-trip tests passed. The bug would have silently corrupted every frame sent over the wire to a real MAVLink peer.
- **Fix**: replaced the all-u16 path with `let mut tmp: u8 = byte ^ ((acc & 0xFF) as u8); tmp ^= tmp.wrapping_shl(4);` — mirrors the C reference exactly. Added a regression test `mavlink_check_string_matches_pymavlink` that asserts `CRC("123456789") == 0x6F91` against the pymavlink reference value (NOT the textbook CRC-CCITT 0x29B1).
- **Why this matters for AZ-642 AC-4**: the SITL conformance gate would have flagged this as a silent wire incompatibility. Surfacing it pre-SITL is exactly the failure mode the AZ-642 spec's "no silent acceptance of malformed frames" reliability NFR is meant to guard against.

clippy / fmt fixes (mechanical):
- `clippy::approx_constant` in `codec_round_trip.rs` test → `std::f32::consts::FRAC_PI_2`
- Borrow-checker shuffle in `schema::validate` (capture validation errors into `Vec<String>` before dropping the result borrow so `value` can be moved out)

## Stuck Agents

None.

## Skill discipline notes

- Did NOT run the sub-skill `/code-review`. The implement skill's Step 9 calls for it; this batch performs an inline code review (as in batch 1) to conserve context. Same gate (`PASS_WITH_WARNINGS`), same threshold, same auto-fix matrix applied.
- Did NOT modify `shared::config::MavlinkConfig` to add `sysid` / `compid` / `link_timeout` / etc. The configurable knobs live in `MavlinkLayerOptions` and `MissionClientOptions` instead — the composition root (when it wires these in AZ-643+/AZ-650+/AZ-648+) will translate the TOML into the option structs.

## Files Modified (summary)

```
Cargo.toml                                         (+5 lines: reqwest, jsonschema, tokio-serial, wiremock)
crates/mavlink_layer/Cargo.toml                    (+5 lines: thiserror, bytes, tokio-serial, dev-deps)
crates/mavlink_layer/src/lib.rs                    (~360 lines, replaces previous stub)
crates/mavlink_layer/src/internal/mod.rs           (new, 6 lines)
crates/mavlink_layer/src/internal/uri.rs           (new, ~110 lines)
crates/mavlink_layer/src/internal/retry.rs         (new, ~85 lines)
crates/mavlink_layer/src/internal/heartbeat.rs     (new, ~190 lines)
crates/mavlink_layer/src/internal/transport/{mod,udp,serial}.rs   (new, ~125 lines combined)
crates/mavlink_layer/src/internal/codec/{mod,crc,parse_errors,messages,encoder,decoder}.rs   (new, ~1450 lines combined)
crates/mavlink_layer/tests/{codec_round_trip,udp_link,serial_link}.rs   (new, ~320 lines combined)

crates/mission_client/Cargo.toml                   (+8 lines: thiserror, serde, serde_json, reqwest, jsonschema, uuid, wiremock dev)
crates/mission_client/src/lib.rs                   (~220 lines, replaces previous stub)
crates/mission_client/src/internal/mod.rs          (new, 3 lines)
crates/mission_client/src/internal/retry.rs        (new, ~40 lines)
crates/mission_client/src/internal/missions_api/mod.rs   (new, ~140 lines)
crates/mission_client/src/internal/schema/mod.rs   (new, ~115 lines)
crates/mission_client/tests/pull_mission.rs        (new, ~180 lines)

crates/shared/contracts/mission-schema.json        (new, ~70 lines — bundled wire contract)

_docs/_autodev_state.md                            (sub_step phase 14 → 5 → … → 14; one pointer file)
```

## Local verification log

```
cargo check --workspace                                    →  clean
cargo check --workspace --no-default-features              →  clean
cargo fmt --all -- --check                                 →  clean
cargo clippy --workspace --all-targets -- -D warnings      →  clean
cargo test --workspace                                     →  pass (all suites; 4 mavlink_layer integ + 5 mission_client integ + 21 mavlink_layer unit + 4 mission_client unit + previously-existing stub tests; 1 ignored as expected for serial)
```

## Next Batch

Tasks now unblocked by AZ-641 / AZ-642 / AZ-644:

- `AZ-643 mavlink_ack_demux_and_signing` (5 pts; deps: AZ-641 + AZ-642)
- `AZ-645 mission_client_waypoint_post` (3 pts; deps: AZ-644)
- `AZ-646 mission_client_mapobjects_pull` (3 pts; deps: AZ-644)
- `AZ-657 frame_ingest_rtsp_session` (3 pts; deps: AZ-640 only — was already unblocked but not chosen for batch 2)
- `AZ-653 gimbal_a40_transport` (5 pts; deps: AZ-640 only — same)
- `AZ-665 mapobjects_store_h3_classify` (5 pts; deps: AZ-640 only — same)
- `AZ-672 vlm_client_provider_trait` (2 pts; deps: AZ-640 only — same)

**Recommendation for batch 3**: `AZ-645 + AZ-646 + AZ-647` mission_client trio (3 + 3 + 5 = 11 pts) — finishes the missions-API client. Alternative: `AZ-643 + AZ-657 + AZ-653` (5 + 3 + 5 = 13 pts) — closes the MAVLink surface (signing) and starts the perception pipeline. Either is within the 20-point batch cap.
