[AZ-643] [AZ-665] [AZ-672] mavlink+mapobjects+vlm batch 4
ci/woodpecker/push/build-arm Pipeline failed

AZ-643 mavlink_layer:
- ack demux on COMMAND_LONG/COMMAND_ACK with oneshot dispatch and
  configurable deadline; MavlinkHandle::send_command + SendCommandError
- MAVLink-2 signing: Signer/Verifier built on SHA-256, key + timestamp
  source, incompat-flag wiring in encoder, reject + counter in decoder
- new tests: tests/ack_demux.rs (3) + tests/signing.rs (5)

AZ-665 mapobjects_store:
- internal/h3_index.rs (h3o wrapper, cell_of, grid_disk, haversine)
- internal/store.rs (in-memory (cell -> Vec<MapObject>) hashmap with
  k-ring classify and class-group resolution)
- public API: MapObjectsStoreHandle::classify(ClassifyInput) ->
  Classification {New|Moved|Existing}
- AC1-4 in tests/classify.rs; AC5 perf gate (#[ignore], passes in
  --release)

AZ-672 vlm_client + autopilot:
- DisabledVlmProvider in shared::contracts; VlmProvider::name() for
  composition-root diagnostics
- vlm_client::VlmClient gated behind feature = "vlm"; placeholder
  until AZ-673 lands the real NanoLLM IPC
- autopilot: vlm_client is now optional = true under feature vlm;
  Runtime::select_vlm_provider picks DisabledVlmProvider when feature
  off OR config.vlm.enabled = false

Workspace deps: +sha2 (mavlink signing), +h3o (mapobjects index).
Batch report: _docs/03_implementation/batch_04_cycle1_report.md

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-19 13:31:42 +03:00
parent 0a87c0f716
commit 69c0629350
29 changed files with 2492 additions and 131 deletions
+63 -3
View File
@@ -28,12 +28,44 @@ pub trait MavlinkSink: Send + Sync {
async fn send_raw(&self, msg: Vec<u8>) -> Result<()>;
}
/// Tier-3 visual-language-model provider. Default impl in `vlm_client` returns
/// `VlmAssessment { status: Disabled, label: Inconclusive, ... }` when the
/// `vlm` feature is off, satisfying the optionality contract.
/// Tier-3 visual-language-model provider.
///
/// The default impl `DisabledVlmProvider` (also in this module) returns
/// `VlmAssessment { status: Disabled, label: Inconclusive, ... }` and is
/// the only path available when the binary is built without the `vlm`
/// feature, or when `config.vlm.enabled = false` at runtime. The real
/// IPC path lives in the optional `vlm_client` crate.
#[async_trait]
pub trait VlmProvider: Send + Sync {
async fn assess(&self, roi: Vec<u8>, prompt: String) -> Result<VlmAssessment>;
/// Diagnostic name for the resolved provider. Used by the runtime
/// composition root for `/health` and tracing. Implementations
/// should return a stable kebab-case identifier; the default value
/// is `"unknown"`.
fn name(&self) -> &'static str {
"unknown"
}
}
/// Zero-sized `VlmProvider` that always returns `VlmAssessment::disabled()`.
///
/// Available regardless of the `vlm` Cargo feature — `scan_controller` and
/// the composition root depend only on `VlmProvider`, never on the
/// optional `vlm_client` crate, so the binary can install this even when
/// `vlm_client` is not compiled in.
#[derive(Debug, Default, Clone, Copy)]
pub struct DisabledVlmProvider;
#[async_trait]
impl VlmProvider for DisabledVlmProvider {
async fn assess(&self, _roi: Vec<u8>, _prompt: String) -> Result<VlmAssessment> {
Ok(VlmAssessment::disabled())
}
fn name(&self) -> &'static str {
"disabled"
}
}
/// Operator-command dispatch. Implemented by `operator_bridge`, fed by the
@@ -42,3 +74,31 @@ pub trait VlmProvider: Send + Sync {
pub trait OperatorCommandSink: Send + Sync {
async fn dispatch(&self, command: OperatorCommand) -> Result<()>;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::vlm::VlmStatus;
#[tokio::test]
async fn ac1_disabled_provider_returns_disabled_status() {
// Arrange
let p = DisabledVlmProvider;
// Act
let start = std::time::Instant::now();
let result = p
.assess(Vec::new(), String::new())
.await
.expect("disabled path is infallible");
let elapsed = start.elapsed();
// Assert
assert_eq!(result.status, VlmStatus::Disabled);
assert!(
elapsed <= std::time::Duration::from_millis(1),
"expected ≤1 ms, got {elapsed:?}",
);
assert_eq!(p.name(), "disabled");
}
}