[AZ-649] [AZ-674] [AZ-667] telemetry + vlm schema + mapobjects hydrate batch 6

AZ-649 mission_executor telemetry forwarding:
- shared::models::telemetry::UavTelemetry canonical model
- TelemetryForwarder with atomic ArcSwap snapshot + 3 lossy
  tokio::sync::broadcast channels (MissionExecutor, ScanController,
  MavlinkUplink) + per-consumer drop counters
- MavlinkProjection::from_mavlink for HEARTBEAT/GLOBAL_POSITION_INT/
  ATTITUDE/SYS_STATUS
- spawn_mavlink_pump bridges mavlink_layer into the forwarder at the
  binary edge

AZ-674 vlm_client schema validation + model_version tracking:
- AssessmentParser owns schema validation + model-version state
- wire::read_response_raw splits raw bytes from parsing so invalid
  payloads can be logged size-capped
- VlmStatus gains an Inconclusive variant; exhaustive-match test
  guards downstream consumers
- VlmPipelineStatus mirrors the new variant in shared::models::poi

AZ-667 mapobjects_store hydrate + pending logs + cascade:
- SyncState enum aligned with description.md (FreshBoot, Synced,
  CachedFallback, Degraded, Failed)
- Store::hydrate(MapObjectsBundle) replaces in-memory map atomically;
  freshness=Stale -> CachedFallback
- classify() + end_of_pass append MapObjectObservation events to
  pending_observations (New/Moved/Existing/RemovedCandidate)
- apply_decline + LocalAppended ignored items append to pending_ignored
- drain_pending() returns and clears both logs
- cascade_mission(id) purges by_cell + IgnoredSet + pending logs
- Health surface reports sync_state, pending_obs, pending_ign

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-19 17:40:43 +03:00
parent b5cc0c321c
commit e56d428753
26 changed files with 2122 additions and 62 deletions
@@ -1,65 +0,0 @@
# Telemetry Forwarding from Mission Executor
**Task**: AZ-649_mission_executor_telemetry_forwarding
**Name**: Telemetry forwarding to scan, movement, telemetry, BIT input
**Description**: Forward decoded MAVLink telemetry (position, attitude, mode, sys-status) from `mavlink_layer` to `scan_controller` (proximity + middle-waypoint computation), `movement_detector` (ego-motion compensation), and `telemetry_stream` (operator overlay). Provide a typed `UavTelemetry` snapshot for BIT consumption.
**Complexity**: 2 points
**Dependencies**: AZ-640_initial_structure, AZ-648_mission_executor_state_machine
**Component**: mission_executor
**Tracker**: AZ-649
**Epic**: AZ-636
## Problem
`mission_executor` is the only component subscribed to the raw decoded MAVLink stream — it owns the airframe relationship. Downstream components (`scan_controller`, `movement_detector`, `telemetry_stream`) and the BIT path need the same telemetry, but in a typed, projection-friendly form (`UavTelemetry { position, attitude, mode, sys_status, monotonic_ts }`). Forwarding must not duplicate decode work and must not drop messages silently.
## Outcome
- `UavTelemetry` is published on three lossy broadcast channels (one per downstream consumer) with monotonic timestamps; consumers that fall behind get drops counted, not blocking.
- `UavTelemetrySnapshot` (latest-state view) is exposed for BIT and health-check consumers.
- Health surface: `last_telemetry_ts`, per-consumer drop counters.
## Scope
### Included
- Subscribe to the typed `MavlinkMessage` enum from `mavlink_layer`.
- Project to `UavTelemetry` (`data_model.md §UavTelemetry`).
- Publish on three Tokio broadcast channels.
- Maintain an atomic latest-snapshot for synchronous reads.
### Excluded
- Decoding MAVLink (task 03).
- Geofence/battery checks (task 13).
- BIT logic (task 11).
## Acceptance Criteria
**AC-1: Telemetry reaches all three consumers**
Given a healthy SITL link
When `GLOBAL_POSITION_INT` and `ATTITUDE` arrive at 10 Hz
Then `UavTelemetry` is observed at ≥10 Hz on all three downstream channels, with monotonic timestamps.
**AC-2: Slow consumer drops, fast consumers unaffected**
Given a slow consumer that yields every 500 ms while telemetry arrives at 10 Hz
When the channels back-pressure
Then the slow consumer's drop counter increments while the other two channels deliver every frame.
**AC-3: Latest-snapshot is monotonic**
Given a sequence of telemetry messages with monotonically advancing timestamps
When `latest_snapshot()` is read concurrently
Then every read returns a snapshot whose `monotonic_ts` is `>=` the previously observed value.
## Non-Functional Requirements
**Performance**
- Telemetry republish adds ≤2 ms to the MAVLink decode-to-consumer path.
**Reliability**
- Slow consumer never blocks fast consumers (lossy broadcast).
- Drops are counted, never silent.
## Runtime Completeness
- **Named capability**: typed telemetry fan-out to three concurrent consumers.
- **Production code that must exist**: real Tokio broadcast or equivalent; real atomic snapshot.
- **Unacceptable substitutes**: blocking single-consumer queue is not acceptable (it would gate the slowest downstream).
@@ -1,80 +0,0 @@
# Pre-Flight Hydrate + Sync State Machine + Pending Logs
**Task**: AZ-667_mapobjects_store_hydrate_and_pending
**Name**: Pre-flight hydrate from MapObjectsBundle + sync_state machine + pending_observations/pending_ignored append logs
**Description**: Hydrate the store from a `MapObjectsBundle` (from `mission_client`'s pull). Maintain a `sync_state` enum (`synced | cached_fallback | degraded | failed`). Append every NEW / MOVED / EXISTING / REMOVED-CANDIDATE / IgnoredItem event to `pending_observations` / `pending_ignored` for the post-flight push.
**Complexity**: 5 points
**Dependencies**: AZ-640_initial_structure, AZ-665_mapobjects_store_h3_classify, AZ-666_mapobjects_store_ignored_and_pass_sweep
**Component**: mapobjects_store
**Tracker**: AZ-667
**Epic**: AZ-633
## Problem
The on-device working copy is hydrated pre-flight from the central API. The sync_state machine (`fresh_boot → synced | cached_fallback | degraded`) tracks the relationship to the central source of truth. During flight, every classification event is appended to `pending_observations` (or, for declines, `pending_ignored`) — central writes are forbidden mid-flight (Frozen choice 6). The pending logs feed the post-flight push.
## Outcome
- `hydrate(bundle: MapObjectsBundle) -> Result<()>` loads the bundle into the in-memory hashmap + IgnoredSet; sets `sync_state = synced` (or `cached_fallback` if `bundle.fallback_used`).
- `on_classify_result(classification, detection)` appends a `MapObjectObservation` to `pending_observations` for NEW / MOVED / EXISTING / REMOVED-CANDIDATE.
- `on_decline(ignored_item)` appends to `pending_ignored`.
- `drain_pending() -> (Vec<MapObjectObservation>, Vec<IgnoredItem>)` is called by `mission_client::push_mapobjects_diff` post-flight.
- Health surface: `sync_state`, `pending_observations_count`, `pending_ignored_count`, `last_pull_ts`, `last_push_ts`.
- On `DELETE /missions/{id}` cascade signal from `mission_client`, drop mission-scoped objects.
## Scope
### Included
- `MapObjectsBundle` hydration (model = `data_model.md §MapObjectsBundle`).
- Sync-state enum + transitions.
- Append-only `pending_observations` + `pending_ignored` logs (in-memory; durable disk handoff lives in `mission_client` task 08).
- Drain API.
- Mission-cascade handler.
### Excluded
- H3 classify (task 26).
- Disk persistence (task 29) — this task keeps pending in memory + lets `mission_client` task 08 handle disk durability.
- Post-flight push (lives in `mission_client` task 08).
## Acceptance Criteria
**AC-1: Hydrate from bundle**
Given a `MapObjectsBundle` with N MapObjects and M IgnoredItems
When `hydrate(bundle)` is called
Then the store contains all N + M entries and `sync_state = "synced"`.
**AC-2: Fallback bundle sets cached_fallback**
Given a bundle with `fallback_used = true`
When `hydrate(bundle)` is called
Then `sync_state = "cached_fallback"`.
**AC-3: Classify appends pending observation**
Given the store hydrated and a detection that classifies as `New`
When `on_classify_result(New, detection)` is called
Then `pending_observations_count` increments by 1.
**AC-4: Drain returns and clears pending**
Given pending_observations_count = 5, pending_ignored_count = 2
When `drain_pending()` is called
Then it returns 5 observations + 2 ignored items; counts return to 0.
**AC-5: Cascade drops mission-scoped objects**
Given `M1` (mission A) and `M2` (mission B) objects in the store
When the cascade signal for mission A arrives
Then `M1` is dropped; `M2` remains.
## Non-Functional Requirements
**Performance**
- Hydrate from a 30 km × 30 km bundle: ≤2 s (peer of pre-flight pull's 30 s budget).
- Append per classification: ≤100 µs.
## Contract
- Canonical typed model: `data_model.md §MapObjectsBundle`, `§MapObjectObservation`.
## Runtime Completeness
- **Named capability**: hydrate + sync_state + pending event logs.
- **Production code that must exist**: real hydrate; real pending append; real drain.
- **Unacceptable substitutes**: central writes mid-flight are forbidden (Frozen choice 6).
@@ -1,72 +0,0 @@
# VlmAssessment Schema Validation + Model-Version Tracking
**Task**: AZ-674_vlm_client_schema_and_model_version
**Name**: VlmAssessment schema validation + model_version tracking + status enum coverage
**Description**: Validate every NanoLLM response against the `VlmAssessment` schema. On schema-invalid, return `status: schema_invalid` + log the raw response (size-capped) for offline analysis. Capture `model_version` on every assessment for forensic correlation; log on change.
**Complexity**: 3 points
**Dependencies**: AZ-640_initial_structure, AZ-673_vlm_client_nanollm_ipc
**Component**: vlm_client
**Tracker**: AZ-674
**Epic**: AZ-631
## Problem
The NanoLLM process emits free-form text, but the autopilot consumes ONLY a validated structured `VlmAssessment`. Schema-invalid responses MUST not propagate as malformed evidence — they're returned as `status: schema_invalid` with the raw response logged size-capped for offline analysis. Model-version capture supports forensic correlation when an assessment's quality is later disputed.
## Outcome
- `VlmAssessmentParser::parse(raw_response) -> VlmAssessment` validates the response against the schema; on failure returns `VlmAssessment { status: SchemaInvalid, .. }` and logs the raw response (size-capped to e.g. 4 KB) at warn level.
- `model_version` field is populated on every assessment from the NanoLLM-reported version; changes are logged at info level once per change.
- Status enum exhaustively covers `Ok | Inconclusive | Timeout | SchemaInvalid | IpcError | Disabled`; consumer match-exhaustion is enforced by the type.
## Scope
### Included
- Schema definition in `shared/contracts/vlm-assessment.json` (or equivalent Rust schema).
- Parser implementation.
- Model-version change detection.
- Size-capped raw-response logging.
### Excluded
- The UDS transport (task 34).
- Provider trait wiring (task 33).
## Acceptance Criteria
**AC-1: Valid response parses successfully**
Given a fixture NanoLLM response with all required fields
When `parse(raw)` runs
Then it returns `VlmAssessment { status: Ok, label, confidence, model_version, .. }`.
**AC-2: Schema-invalid response returns schema_invalid + logs**
Given a fixture response missing a required field
When `parse(raw)` runs
Then it returns `VlmAssessment { status: SchemaInvalid, .. }` and the raw response excerpt (size-capped) is observable in log output.
**AC-3: Model version change logged once**
Given an assessment with `model_version = "v1.0"` followed by another with `model_version = "v1.1"`
When the change is detected
Then a single log entry observes the change; subsequent assessments with `v1.1` do NOT re-log.
**AC-4: Status enum is exhaustive**
Given consumer code that matches on `VlmAssessment.status`
When a new variant is added (compile-time)
Then the compiler forces handling of the new variant; no `_ => …` catch-all in the policy code-path.
## Non-Functional Requirements
**Performance**
- Schema validation: ≤2 ms.
**Reliability**
- Schema mismatches never silent.
## Contract
- Canonical typed model: `data_model.md §VlmAssessment`. Schema lives at `shared/contracts/vlm-assessment.json`.
## Runtime Completeness
- **Named capability**: VlmAssessment schema validation + model-version awareness.
- **Production code that must exist**: real schema validator; real model-version tracker.
- **Unacceptable substitutes**: silently mapping a schema-invalid response to `status: Ok` with placeholder fields is unacceptable.