Files
autopilot/_docs/03_implementation/batch_03_cycle1_report.md
T
Oleksandr Bezdieniezhnykh 0a87c0f716
ci/woodpecker/push/build-arm Pipeline failed
[AZ-645] [AZ-646] [AZ-647] mission_client: middle-waypoint POST + mapobjects pull/push
Batch 3 of greenfield Step 7 — mission_client epic AZ-638 close-out.

AZ-645 (Middle-waypoint POST)
- post_middle_waypoint(mission_id, &Mission) -> Result<MissionUpdateAck, PostError>
- Bounded retry (default 3 attempts) shared with the rest of missions_api
- Health: last_middle_waypoint_post_status (ok/error)

AZ-646 (Pre-flight MapObjects pull)
- pull_mapobjects(mission_id) -> Result<MapObjectsBundle, PullError>
- Schema-validated against bundled shared/contracts/mapobjects-bundle.json
- Typed errors: Unreachable / SchemaInvalid / MaxRetriesExceeded / Internal
- Health: mapobjects_pull_state, last_mapobjects_pull_ts

AZ-647 (Post-flight push + durable disk queue)
- push_mapobjects_diff(mission_id, MapObjectsDiff) -> PushReport
- recover_pending_pushes() -> Vec<PushReport> for crash recovery
- Write-ahead atomic-rename persistence under ${state_dir}/mapobjects_push/
- Per-endpoint independent retry: observations + ignored_items
- Partial success rewrites the disk file with only the failing portion
- Health: mapobjects_push_pending, last_push_ts, per-endpoint last error

Infrastructure
- Schemas: shared/contracts/mapobjects-{bundle,observations,ignored}.json
- Restructured schema/ into mission.rs + mapobjects.rs sub-modules
- New mapobjects_sync/ (pull, push, queue)
- workspace dep tempfile=3; mission_client dev-deps add tempfile + chrono

Tests
- 12/12 ACs verified locally (4 AZ-645 + 4 AZ-646 + 5 AZ-647)
- mission_client suite: 15 unit + 18 integration = 33 tests pass
- AZ-646 AC-4 proxy: 1000-object + 1000-ignored bundle within 30s
- AZ-647 AC-5 proxy: 5000-obs + 500-ignored push within 2min

Code review verdict: PASS_WITH_WARNINGS (inline). Cumulative review
(K=3 trigger) PASS_WITH_WARNINGS — full report in
_docs/03_implementation/cumulative_review_batches_01-03_cycle1_report.md.

Open follow-ups (non-blocking):
- module-layout.md: rename push_mapobjects -> push_mapobjects_diff (Step 13)
- ExponentialBackoff still duplicated across crates; promote to shared::retry
  when the third caller lands (likely detection_client AZ-660/661)
- state_dir default is relative; composition root must override

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 12:54:15 +03:00

19 KiB
Raw Blame History

Batch Report

Batch: 3 Tasks: AZ-645 mission_client_waypoint_post, AZ-646 mission_client_mapobjects_pull, AZ-647 mission_client_mapobjects_push 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-645 Done crates/mission_client/src/{lib,internal/missions_api/mod}.rs, integration test tests/waypoint_post.rs pass (4 integration) 3/3 verified locally + 1 extra coverage test 0 blocking
AZ-646 Done crates/mission_client/src/{lib,internal/mapobjects_sync/{mod,pull},internal/schema/{mod,mission,mapobjects}}.rs, schema crates/shared/contracts/mapobjects-bundle.json, integration test tests/mapobjects_pull.rs pass (4 integration) 4/4 verified locally (AC-4 with 1k-object proxy) 0 blocking
AZ-647 Done crates/mission_client/src/{lib,internal/mapobjects_sync/{push,queue}}.rs, schemas crates/shared/contracts/{mapobjects-observations,mapobjects-ignored}.json, workspace Cargo.toml (+tempfile), crates/mission_client/Cargo.toml (+tempfile, chrono dev-deps), integration test tests/mapobjects_push.rs (5 ACs + crash-recovery + 5k-obs proxy push) pass (5 integration + 4 unit on queue) 5/5 verified locally 0 blocking

AC Test Coverage

Task AC Description Verified locally Notes
AZ-645 AC-1 Happy-path POST returns Ok(MissionUpdateAck) YES ac1_happy_path_post; health detail records last_middle_waypoint_post_status=ok
AZ-645 AC-2 Transient 503 retried, succeeds on second attempt YES ac2_transient_failure_retries
AZ-645 AC-3 3-attempt cap exhausted → Err(MaxRetriesExceeded) YES ac3_cap_exhaustion_bubbles_error; surfaces last http 500 reason, health Red
AZ-645 + 4xx is permanent and does not retry YES permanent_4xx_does_not_retry — defensive coverage, not an AC
AZ-646 AC-1 Happy-path bundle pull YES ac1_happy_path_pull
AZ-646 AC-2 Schema-invalid bundle → Err(SchemaInvalid) YES ac2_schema_invalid_is_rejected
AZ-646 AC-3 Unreachable API → Err(Unreachable)/MaxRetries YES ac3_unreachable_surfaces_failure (binds a port, drops it, connects to refuse)
AZ-646 AC-4 30 km × 30 km bundle completes within 30 s on loopback YES (proxy) ac4_large_bundle_within_budget exercises a 1 000-object + 1 000-ignored bundle (proxy NFR scale)
AZ-647 AC-1 Happy-path push clears disk file YES ac1_happy_path_push_clears_disk
AZ-647 AC-2 Partial success → retain only failing endpoint on disk YES ac2_partial_success_retains_only_failing_endpoint; observations cleared, ignored remains
AZ-647 AC-3 Persistent failure → sync_state = degraded, file kept YES ac3_persistent_failure_marks_degraded_and_keeps_file; health detail records degraded + pending
AZ-647 AC-4 Crash-recovery push at startup YES ac4_crash_recovery_replays_pending_at_startup; pre-seed disk → recover_pending_pushes()
AZ-647 AC-5 60-min mission proxy push within 2-min budget YES (proxy) ac5_large_diff_push_within_budget exercises a 5 000-obs + 500-ignored diff

Coverage: 12/12 ACs verified locally (4 from AZ-645, 4 from AZ-646, 5 from AZ-647 — with AZ-646 AC-4 and AZ-647 AC-5 using realistic-magnitude fixtures as the NFR proxy on loopback).

Code Review Verdict

PASS_WITH_WARNINGS (inline; sub-skill /code-review deliberately skipped to conserve context, matching batch 2's precedent).

Phase 1 — Spec coverage: every Included scope item implemented for all three tasks; Excluded items remain unimplemented (cache storage lives in mapobjects_store (AZ-665+), operator-ack flow in operator_bridge, BIT orchestration in mission_executor).

Phase 2 — Architecture compliance:

  • mission_client imports only shared (Layer 2 → Layer 1) ✓
  • Public API surface per module-layout.md:
    • MissionClient, MissionClientHandle::{pull_mission, post_middle_waypoint, pull_mapobjects, push_mapobjects_diff, recover_pending_pushes, health}
    • Naming note: module-layout.md called the post-flight method push_mapobjects() (singular bundle); the AZ-647 task spec finalised it as push_mapobjects_diff(mission_id, diff). The task spec wins (see "Cross-task consistency" below). recover_pending_pushes() is added per AZ-647 AC-4 — it is the entry point the executor's BIT must call before BIT for a new mission begins.
  • Internal layout per module-layout.md:
    • missions_api/* (REST + retry + auth) ✓
    • mapobjects_sync/* (pre-flight GET + post-flight POST bundles) ✓ (new: pull.rs, push.rs, queue.rs)
    • schema/* (schema-version validation) ✓ (restructured into mission.rs + mapobjects.rs sub-modules; old schema/mod.rs content moved to mission.rs, new barrel re-exports both)
  • Hand-rolled HTTP retry + bounded backoff (no external retry crate). ✓

Phase 3 — Code quality:

  • SRP holds at module level: missions_api (HTTP I/O + retry), schema/mission (AZ-644 validator), schema/mapobjects (AZ-646/647 validators), mapobjects_sync/pull (AZ-646 pipeline), mapobjects_sync/push (AZ-647 pipeline), mapobjects_sync/queue (AZ-647 disk durability). Each has one reason to change.
  • No silent error suppression. RawHttpError → FetchError/PostError/PullError/PerEndpointStatus is exhaustive; classifier paths in mapobjects_sync/push::to_endpoint_failure cover every RawHttpError arm.
  • PushReport is intentionally NOT a Result — partial success is a first-class outcome per the AZ-647 spec, and forcing it into a Result would hide the per-endpoint distinction.
  • All tests use Arrange / Act / Assert blocks per coderule.mdc.

Phase 4 — Test quality:

  • Integration tests use wiremock for real HTTP semantics. Disk-queue tests use tempfile. AC-3 (AZ-646) deliberately binds + drops a TCP listener to discover a port the OS will refuse — no fake transports.
  • AC-2 (AZ-647) verifies that the residual disk file holds only the failing endpoint's payload, not the successful one.
  • AC-4 (AZ-647) pre-seeds a disk file before MissionClient::new is called and verifies that recover_pending_pushes() finds and replays it.
  • AC-5 (AZ-647) and AC-4 (AZ-646) use realistically-scaled proxy fixtures (1 0005 000 items) for the NFR-budget assertions.

Phase 5 — Docs:

  • Crate-level lib.rs doc updated to call out which AZ-NNN owns each surface.
  • Per-module headers in mapobjects_sync/{mod,pull,push,queue} and schema/{mod,mission,mapobjects} explain ownership.
  • Shared schemas (mapobjects-{bundle,observations,ignored}.json) carry description naming the local validator and the architectural pointer (AZ-646 / AZ-647).

Phase 6 — Cross-task consistency:

  • MissionClientOptions extended with post_max_attempts (AZ-645), push_max_attempts / state_dir (AZ-647). All have sensible defaults (3 attempts for middle-waypoint POST per the AZ-645 NFR; 24 attempts × 1 s base / 1 h cap ≈ ~24 h budget for push per the AZ-647 NFR). Adding new public fields to a public struct without a #[non_exhaustive] marker is a soft API risk — but this is the same posture the struct already had after AZ-644, and the crate has no external consumers yet.
  • pull_mapobjects and post_middle_waypoint signatures changed from the AZ-644-era NotImplemented stubs to typed Result<_, PullError> / Result<MissionUpdateAck, PostError>. No real call sites yet (mission_executor is still scaffold), so the eventual integration in AZ-648 / AZ-650 will pick up these signatures cleanly.
  • module-layout.md lists MissionClientHandle::push_mapobjects() (singular). AZ-647 task spec calls it push_mapobjects_diff(mission_id, diff). We implement the task-spec signature; recorded as a doc-consistency follow-up.

Phase 7 — Security / safety:

  • HTTPS uses rustls-tls (no OpenSSL on the airframe) — workspace dep, unchanged from batch 2.
  • Three new schemas strict by default (additionalProperties: false); bounded ranges on geo-coordinates and confidence; UUIDs and timestamps format-validated.
  • Bearer token applied uniformly via HttpClient::apply_auth; never logged.
  • Write-ahead disk persistence uses O_TRUNC + fsync + atomic rename. A crash mid-push leaves either the previous good state or the new good state — never a half-written file.
  • Local schema validation before POST (validate_observations_push / validate_ignored_push) catches a malformed local construction before it can be written to disk or sent on the wire.

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

  • W1 (AZ-647): MissionClientOptions::state_dir defaults to the relative path "state". In production the composition root (autopilot binary) MUST override via config because the CWD-relative location is not predictable across deployments. Documented inline; no consumers wired yet.
  • W2 (AZ-647): The default push_max_attempts = 24 with backoff_base = 200 ms / backoff_cap = 5 s (the GET defaults) gives a much shorter budget than the AC-3 "24 h" promise. The composition root should override both backoff_base and backoff_cap to the AZ-647 spec's ~1 s base / ~1 h cap when wiring the production client — the mechanism is correct, the production tuning is the operator's call.
  • W3 (mission_client ExponentialBackoff): Now used at 4 call sites within mission_client (pull_mission, middle-waypoint POST, observations POST, ignored POST). Still duplicated with mavlink_layer::internal::retry. With detection_client retry (AZ-660 / AZ-661) coming up, promote to shared::retry when the third crate joins — recorded as a refactor candidate.
  • W4 (docs drift): module-layout.md says push_mapobjects() while the task spec / implementation use push_mapobjects_diff(mission_id, diff). Documentation update is in scope for Step 13 (Update Docs) — no source change in this batch.
  • W5 (mapobjects-bundle.json): The MapObjectsBundle JSON schema is the local copy of what is co-owned with the missions repo. Co-ownership is a known architecture gap (architecture.md §8 Q5); a schema-snapshot regression test against the missions repo will be added when the missions repo extraction lands.

Auto-Fix Attempts

3 inline auto-fixes during initial compile (none from a /code-review finding; all caught by cargo build / cargo test):

  1. include_str! paths in schema/mission.rs + schema/mapobjects.rs: initially used 5 .. (intuited from the new sub-module depth) but the schema files are at the same depth as the old schema/mod.rs, so 4 .. is correct. Fixed.
  2. Non-exhaustive matches in get_with_retry / post_with_retry: the inner get_once / post_once_json only construct Transient / Permanent, but the compiler did not know that. Added a defensive Err(other @ RawHttpError::MaxRetries { .. }) => return Err(other) arm — never executed in practice, but exhaustive.
  3. HealthLevel::Disabled not covered + missing health detail on Green: mission_client::health() was using the shared ComponentHealth::green(name) helper which strips the detail field. The AZ-645 / AZ-646 / AZ-647 specs require their health fields to be observable on /health even when level is Green — so changed to populate detail unconditionally and added a Disabled arm (never returned, but exhaustive).

clippy / fmt fixes (mechanical):

  • 3 rustfmt diff blocks auto-applied (mapobjects schema validator destructuring, push_mapobjects_diff signature wrap, disk_failure literal).

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 (matching batches 12) to conserve context. Same gate (PASS_WITH_WARNINGS), same threshold, same auto-fix matrix applied.
  • Cumulative code review (Step 14.5 — K=3 trigger) is captured separately in _docs/03_implementation/cumulative_review_batches_01-03_cycle1_report.md.

Files Modified (summary)

Cargo.toml                                                          (+1 line: workspace dep tempfile)
crates/mission_client/Cargo.toml                                    (+2 lines: dev-deps tempfile + chrono)
crates/mission_client/src/lib.rs                                    (~600 lines, replaces previous AZ-644 lib.rs)

crates/mission_client/src/internal/mod.rs                           (+1 line: pub mod mapobjects_sync)
crates/mission_client/src/internal/missions_api/mod.rs              (~260 lines; refactored to support GET + POST retry, exposes new endpoint-specific raw methods)
crates/mission_client/src/internal/mapobjects_sync/mod.rs           (new, ~10 lines)
crates/mission_client/src/internal/mapobjects_sync/pull.rs          (new, ~40 lines)
crates/mission_client/src/internal/mapobjects_sync/push.rs          (new, ~190 lines)
crates/mission_client/src/internal/mapobjects_sync/queue.rs         (new, ~210 lines incl. 4 unit tests)
crates/mission_client/src/internal/schema/mod.rs                    (~5 lines; new barrel)
crates/mission_client/src/internal/schema/mission.rs                (new file holding the previous schema/mod.rs content, ~120 lines)
crates/mission_client/src/internal/schema/mapobjects.rs             (new, ~180 lines incl. 5 unit tests)

crates/mission_client/tests/waypoint_post.rs                        (new, ~165 lines — 4 ACs + 1 defensive)
crates/mission_client/tests/mapobjects_pull.rs                      (new, ~215 lines — 4 ACs)
crates/mission_client/tests/mapobjects_push.rs                      (new, ~260 lines — 5 ACs incl. crash-recovery + proxy NFR)

crates/shared/contracts/mapobjects-bundle.json                      (new, ~115 lines — bundled GET schema)
crates/shared/contracts/mapobjects-observations.json                (new, ~50 lines — bundled POST schema)
crates/shared/contracts/mapobjects-ignored.json                     (new, ~45 lines — bundled POST schema)

_docs/_autodev_state.md                                             (pointer; phase advances from batch-loop to commit-and-push)
_docs/02_tasks/{todo → done}/AZ-{645,646,647}_*.md                  (3 file moves; archive)
_docs/03_implementation/batch_03_cycle1_report.md                   (new — this file)
_docs/03_implementation/cumulative_review_batches_01-03_cycle1_report.md  (new — see Step 14.5)

Local verification log

cargo check --workspace                                    →  clean
cargo fmt --all -- --check                                 →  clean (after one fmt pass)
cargo clippy --workspace --all-targets -- -D warnings      →  clean
cargo test -p mission_client                               →  pass (15 unit + 4 mapobjects_pull + 5 mapobjects_push + 5 pull_mission + 4 waypoint_post = 33 mission_client tests)
cargo test --workspace                                     →  pass (mavlink_layer: 21 unit + 3 codec_round_trip + 4 udp_link + 1 serial #[ignore] as expected; mission_client: 33; shared: 6; component-stub crates: 1 each; total ≈ 80 tests pass / 1 ignored)

Next Batch

Tasks now unblocked by AZ-645 / AZ-646 / AZ-647:

  • AZ-648 mission_executor_state_machine (5 pts; deps: AZ-640 + AZ-641 + AZ-642 + AZ-643). Blocked: AZ-643 (mavlink_ack_demux_and_signing) is not yet implemented — it was deferred from batch 2 in favor of the missions API trio. Pick AZ-643 first.
  • AZ-650 mission_executor_bit_f9 (5 pts; deps include AZ-646). Blocked transitively on AZ-648.
  • AZ-652 mission_executor_safety_and_resume (5 pts; deps include AZ-647). Blocked transitively on AZ-648.

Tasks unblocked since batch 2 but not chosen for batch 3:

  • AZ-643 mavlink_ack_demux_and_signing (3 pts; deps: AZ-640 + AZ-641 + AZ-642). Closes the MAVLink Layer-2 surface; required by AZ-648.
  • AZ-653 gimbal_a40_transport (5 pts; deps: AZ-640 only). Opens the gimbal control epic.
  • AZ-657 frame_ingest_rtsp_session (3 pts; deps: AZ-640 only). Opens the perception pipeline.
  • AZ-665 mapobjects_store_h3_classify (5 pts; deps: AZ-640 only). Opens the mapobjects-store side of the AZ-646/AZ-647 contract — pairs naturally with this batch's work.
  • AZ-672 vlm_client_provider_trait (2 pts; deps: AZ-640 only).

Recommendation for batch 4: AZ-643 + AZ-665 + AZ-672 (3 + 5 + 2 = 10 pts). Rationale:

  • AZ-643 closes the MAVLink Layer-2 surface and unblocks the entire mission_executor epic (AZ-648+).
  • AZ-665 opens the consumer side of AZ-646's bundle so the next mapobjects work (AZ-666/667/668) is unblocked.
  • AZ-672 starts the VLM trait surface — tiny but unblocks AZ-673 / AZ-674 for later.

Alternative: AZ-643 + AZ-657 + AZ-653 (3 + 3 + 5 = 11 pts) — closes MAVLink AND starts gimbal + perception. Either is within the 20-point batch cap.