[AZ-626] Decompose complete: 47 tasks + docs + module layout

Greenfield Steps 1-6 baseline for the autopilot rewrite from legacy
Qt/C++ to a Rust workspace.

- Remove legacy Qt/C++ tree (ai_controller, drone_controller,
  misc/camera, python_scaffold, root Dockerfile, autopilot.pro,
  legacy main.py / requirements.txt).
- Add _docs/00_problem (problem, restrictions, acceptance criteria,
  security approach, input data + fixtures).
- Add _docs/01_solution/solution_draft01.
- Add _docs/02_document (architecture, system-flows, data_model,
  glossary, decision-rationale, deployment, 13 component descriptions,
  tests/ specs, FINAL_report, module-layout).
- Add _docs/02_tasks/todo with 47 task specs (AZ-640..AZ-686, one
  bootstrap + 46 component tasks) and _dependencies_table.md.
- Add .cursor/rules/artifact-srp.mdc (single-responsibility rule for
  canonical _docs artifacts).
- Track autodev state in _docs/_autodev_state.md (Step 6 completed,
  ready for Step 7 Implement).

Jira: bootstrap AZ-626; component epics AZ-627..AZ-639; tasks
AZ-640..AZ-686. Total complexity 173 points across 12 epics.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-19 11:02:01 +03:00
parent f7d6cb4a3a
commit bc40ea7300
235 changed files with 12585 additions and 15097 deletions
@@ -0,0 +1,374 @@
# Initial Project Structure
**Task**: AZ-640_initial_structure
**Name**: Initial Structure
**Description**: Scaffold the Rust cargo workspace — per-component crates, shared crate, runtime composition root, Dockerfile + docker-compose for dev/test, Woodpecker CI pipeline, observability scaffold, on-device state directory, env config, and replay-based integration test layout.
**Complexity**: 5 points
**Dependencies**: None
**Component**: Bootstrap
**Tracker**: AZ-640
**Epic**: AZ-626
## Project Folder Layout
```
autopilot/
├── Cargo.toml # cargo workspace manifest
├── Cargo.lock
├── rust-toolchain.toml # pin stable channel + components
├── .cargo/
│ └── config.toml # cross-compile target = aarch64-unknown-linux-gnu
├── .woodpecker.yml # CI pipeline (per deployment/ci_cd_pipeline.md)
├── .dockerignore
├── Dockerfile # multi-stage; non-root; pinned l4t-base for prod, ubuntu:22.04 for emul
├── docker-compose.yml # dev: autopilot + mock detections + mock missions + mock ground-station
├── docker-compose.test.yml # blackbox: autopilot + ArduPilot SITL + mock detections + replay sources
├── .env.example # documented environment variables
├── config/
│ ├── autopilot.dev.toml # dev profile (mock endpoints)
│ ├── autopilot.staging.toml # staging profile (real endpoints, non-flight)
│ └── autopilot.prod.toml # prod template (Jetson on-airframe)
├── crates/
│ ├── autopilot/ # binary crate — runtime composition root
│ │ ├── Cargo.toml # `[[bin]] name = "autopilot"`
│ │ ├── src/
│ │ │ ├── main.rs # CLI parse, config load, wire actors, run
│ │ │ ├── runtime.rs # actor topology, health aggregator, shutdown
│ │ │ └── health_server.rs # HTTP /health endpoint (port from config)
│ │ └── tests/ # cross-crate integration tests (replay-based)
│ ├── shared/
│ │ ├── Cargo.toml
│ │ └── src/
│ │ ├── lib.rs # re-exports
│ │ ├── models/ # canonical entities from data_model.md
│ │ │ ├── mod.rs
│ │ │ ├── frame.rs # Frame, BoundingBox
│ │ │ ├── detection.rs # Detection, DetectionBatch
│ │ │ ├── movement.rs # MovementCandidate
│ │ │ ├── tier2.rs # Tier2Evidence
│ │ │ ├── vlm.rs # VlmAssessment
│ │ │ ├── poi.rs # POI
│ │ │ ├── mapobject.rs # MapObject, MapObjectObservation, MapObjectsBundle, IgnoredItem
│ │ │ ├── mission.rs # MissionItem, MissionWaypoint, Geofence, Coordinate
│ │ │ ├── operator.rs # OperatorCommand
│ │ │ └── gimbal.rs # GimbalState
│ │ ├── config/ # toml loader + typed config sections
│ │ ├── error.rs # AutopilotError enum, Result alias
│ │ ├── health.rs # ComponentHealth, AggregatedHealth
│ │ ├── observability/ # tracing-subscriber init + log field constants
│ │ └── clock.rs # monotonic + wall-clock binding (GPS / NTP)
│ ├── frame_ingest/
│ │ ├── Cargo.toml
│ │ ├── src/lib.rs # public API trait + actor handle
│ │ ├── src/internal/ # decoder, RTSP client
│ │ └── tests/ # replay-based unit tests against fixture RTSP clips
│ ├── detection_client/
│ │ ├── Cargo.toml
│ │ ├── build.rs # tonic-build for ../detections .proto
│ │ ├── proto/ # copy of ../detections gRPC contract
│ │ ├── src/lib.rs
│ │ └── tests/
│ ├── movement_detector/
│ │ ├── Cargo.toml
│ │ ├── src/lib.rs
│ │ ├── src/internal/ # ego-motion, optical-flow, per-zoom-band thresholds
│ │ └── tests/ # replay fixtures, zoom-out + zoom-in
│ ├── semantic_analyzer/
│ │ ├── Cargo.toml
│ │ ├── src/lib.rs
│ │ ├── src/internal/ # primitive graph, ROI CNN call
│ │ └── tests/
│ ├── vlm_client/
│ │ ├── Cargo.toml # feature = ["vlm"] — see autopilot/Cargo.toml
│ │ ├── src/lib.rs # default impl returns VlmAssessment{status=vlm_disabled}
│ │ ├── src/internal/ # UDS client + peer-cred check
│ │ └── tests/
│ ├── scan_controller/
│ │ ├── Cargo.toml
│ │ ├── src/lib.rs
│ │ ├── src/state_machine/ # ZoomedOut / ZoomedIn / TargetFollow types
│ │ ├── src/poi_queue/ # priority queue + ≤5 POIs/min cap
│ │ └── tests/ # behaviour-tree scenarios from system-flows.md §F4
│ ├── mapobjects_store/
│ │ ├── Cargo.toml
│ │ ├── src/lib.rs
│ │ ├── src/internal/h3_index/ # h3rs wrapper
│ │ ├── src/internal/engine/ # engine trait + in-memory+snapshot default impl (Q3)
│ │ └── tests/
│ ├── gimbal_controller/
│ │ ├── Cargo.toml
│ │ ├── src/lib.rs
│ │ ├── src/internal/a40_protocol/ # ViewPro A40 UDP vendor protocol
│ │ └── tests/ # mock A40 over UDP
│ ├── operator_bridge/
│ │ ├── Cargo.toml
│ │ ├── src/lib.rs
│ │ ├── src/internal/auth/ # OperatorCommand envelope validation (Q9 — stubbed)
│ │ └── tests/
│ ├── mission_executor/
│ │ ├── Cargo.toml
│ │ ├── src/lib.rs
│ │ ├── src/internal/multirotor/ # multirotor variant FSM
│ │ ├── src/internal/fixed_wing/ # fixed-wing variant FSM
│ │ ├── src/internal/geofence/ # INCLUSION + EXCLUSION enforcement
│ │ ├── src/internal/failsafe/ # lost-link ladder, battery thresholds
│ │ └── tests/ # ArduPilot SITL fixtures
│ ├── mavlink_layer/
│ │ ├── Cargo.toml
│ │ ├── src/lib.rs
│ │ ├── src/internal/codec/ # MAVLink v2 encode/decode (only §7.7 surface)
│ │ ├── src/internal/transport/ # UDP and serial connection abstraction
│ │ └── tests/ # SITL conformance fixtures
│ ├── mission_client/
│ │ ├── Cargo.toml
│ │ ├── src/lib.rs
│ │ ├── src/internal/missions_api/ # HTTPS REST client; pull + middle-waypoint POST
│ │ ├── src/internal/mapobjects_sync/ # pre-flight GET + post-flight POST of /mapobjects bundles
│ │ └── tests/ # mock missions API
│ └── telemetry_stream/
│ ├── Cargo.toml
│ ├── src/lib.rs
│ ├── src/internal/uplink/ # modem push of frames + telemetry + bbox overlay
│ └── tests/ # mock Ground Station receiver
├── tests/
│ └── e2e/ # cross-crate blackbox scenarios (used by docker-compose.test.yml)
├── benches/
│ ├── tier1_latency.rs # benchmark-gate harness for §6 NFRs
│ ├── tier2_latency.rs
│ ├── gimbal_zoom.rs
│ └── movement_fpr.rs # per-zoom-band FPR replay benchmark
├── fixtures/
│ ├── rtsp/ # pre-recorded RTSP clips
│ ├── mavlink/ # ArduPilot SITL replay scripts
│ ├── missions/ # mission JSON fixtures
│ └── detections/ # deterministic Tier-1 response fixtures
├── deploy/
│ ├── systemd/
│ │ └── autopilot.service # per deployment/containerization.md §3
│ └── jetson/
│ └── README.md # on-airframe install steps
└── README.md
```
### Layout Rationale
- **Cargo workspace with one crate per component** matches the recommended Rust layout in `_docs/02_document/decompose/templates/module-layout.md` (`crates/<component>/`). It enforces module boundaries: a crate's internals (`internal/`, private modules) are unreachable from sibling components — only its `lib.rs` public surface is.
- **Single binary crate `crates/autopilot/`** is the runtime composition root (per `deployment/containerization.md` — "single Rust binary"). It depends on every component crate and wires the actor topology in `runtime.rs`.
- **`crates/shared/`** owns the canonical entity catalogue from `data_model.md` and cross-cutting concerns (config, error, health, observability, clock). All component crates may import from it; it imports from no one.
- **`fixtures/` separate from `tests/`** because the same fixtures feed unit tests, replay-based integration tests, blackbox tests, and benchmark gates.
- **`vlm_client` crate exists unconditionally**; the optional behaviour is implemented via a default `VlmAssessment` provider that returns `status=vlm_disabled` when the `vlm` feature is off (per `architecture.md §7.6` "Optionality model").
## DTOs and Interfaces
### Shared DTOs (live in `crates/shared/src/models/`)
| DTO | Source spec | Used by components |
|---|---|---|
| `Frame`, `BoundingBox` | `data_model.md §2` | `frame_ingest`, `detection_client`, `movement_detector`, `semantic_analyzer`, `telemetry_stream` |
| `Detection`, `DetectionBatch` | `data_model.md §2` | `detection_client`, `scan_controller`, `telemetry_stream`, `operator_bridge` |
| `MovementCandidate` | `data_model.md §2` | `movement_detector`, `scan_controller` |
| `Tier2Evidence` | `data_model.md §2` | `semantic_analyzer`, `scan_controller` |
| `VlmAssessment` | `data_model.md §2` | `vlm_client`, `scan_controller` |
| `POI` | `data_model.md §3` | `scan_controller`, `operator_bridge`, `telemetry_stream` |
| `MapObject`, `MapObjectObservation`, `MapObjectsBundle`, `IgnoredItem` | `data_model.md §3` | `mapobjects_store`, `mission_client`, `scan_controller` |
| `Coordinate`, `Geofence`, `MissionItem` | `data_model.md §4` | `mission_client`, `mission_executor`, `operator_bridge` |
| `MissionWaypoint` | `data_model.md §4` | `mission_executor`, `mavlink_layer` |
| `OperatorCommand` | `data_model.md §4` | `operator_bridge`, `scan_controller`, `mission_executor` |
| `GimbalState` | `data_model.md §4` | `gimbal_controller`, `frame_ingest`, `movement_detector` |
| `AutopilotError`, `Result<T>` | new | every crate |
| `ComponentHealth`, `AggregatedHealth` | new (per `containerization.md §7`) | every crate + `autopilot/runtime.rs` |
### Component Public APIs (live in each component's `lib.rs`)
Each component exposes an actor handle plus its narrow request/response trait. Inter-component communication is Tokio channels owned inside the component; consumers receive a typed handle, not the underlying `tokio::sync::*` types.
| Component | Public surface (handle methods) | Exposed to |
|---|---|---|
| `frame_ingest` | `FrameIngestHandle::subscribe() -> FrameStream`, `health()` | `detection_client`, `movement_detector`, `telemetry_stream` |
| `detection_client` | `DetectionClientHandle::request(Frame) -> Result<DetectionBatch>`, `health()` | `scan_controller`, `movement_detector`, `telemetry_stream` |
| `movement_detector` | `MovementDetectorHandle::candidates() -> CandidateStream`, `health()` | `scan_controller` |
| `semantic_analyzer` | `SemanticAnalyzerHandle::analyze(Roi) -> Result<Tier2Evidence>`, `health()` | `scan_controller` |
| `vlm_client` (trait) | `VlmProvider::assess(Roi) -> Result<VlmAssessment>` (default impl returns `vlm_disabled`) | `scan_controller` |
| `scan_controller` | `ScanControllerHandle::tick(), submit_operator_cmd(OperatorCommand)`, `health()` | `autopilot::runtime` |
| `mapobjects_store` | `MapObjectsStoreHandle::classify(Detection) -> Classification`, `apply_decline(Poi)`, `dump_pending() -> MapObjectsBundle`, `hydrate(MapObjectsBundle)`, `health()` | `scan_controller`, `mission_client` |
| `gimbal_controller` | `GimbalControllerHandle::set_pose(GimbalCommand), zoom(level), state() -> GimbalState`, `health()` | `scan_controller` |
| `operator_bridge` | `OperatorBridgeHandle::surface_poi(POI) -> OperatorDecision`, `cmds() -> CommandStream`, `health()` | `scan_controller`, `mission_executor` |
| `mission_executor` | `MissionExecutorHandle::start(Mission), insert_middle_waypoint(Coordinate), failsafe_trigger(FailsafeKind)`, `health()` | `scan_controller`, `operator_bridge` |
| `mavlink_layer` | `MavlinkHandle::send(Command), telemetry() -> TelemetryStream`, `health()` | `mission_executor`, `telemetry_stream` |
| `mission_client` | `MissionClientHandle::pull_mission() -> Mission`, `post_middle_waypoint(Coordinate)`, `pull_mapobjects(MissionId) -> MapObjectsBundle`, `push_mapobjects(MapObjectsBundle)`, `health()` | `mission_executor`, `mapobjects_store` |
| `telemetry_stream` | `TelemetryStreamHandle::push_frame(Frame, Overlay), push_telemetry(Sample)`, `health()` | `frame_ingest`, `detection_client`, `mavlink_layer`, `operator_bridge` |
## CI/CD Pipeline
Single Woodpecker pipeline (per `deployment/ci_cd_pipeline.md §2`). Stages run sequentially; a failed stage stops the run.
| Stage | Purpose | Tool / Command |
|---|---|---|
| Fetch | Clone, restore Cargo cache | `cargo fetch` with remote cache key |
| Lint | `cargo fmt --check`; `cargo clippy --all-targets --all-features -- -D warnings` | Hard fail on any warning |
| Unit Tests | `cargo test --workspace` (host-arch) | Most logic is platform-independent |
| Build arm64 | Cross-compile for `aarch64-unknown-linux-gnu` | `cross` or `cargo zigbuild`; produce binary + debug symbols |
| Build no-vlm | `cargo build --workspace --no-default-features` | Enforces VLM optionality contract |
| Integration Tests | Replay-based, no hardware | `cargo test --test '*' -- --include-ignored=false`; fixtures from `fixtures/` |
| SITL Conformance | ArduPilot SITL + autopilot binary in containers, fixed mission, asserts §7.7 surface + geofence | `docker compose -f docker-compose.test.yml up --abort-on-container-exit` |
| Security Scan | `cargo audit` + `cargo deny check` | Dependency CVE scan |
| Benchmark Gate (manual / nightly) | Tier 1 / 2 / VLM / gimbal latency on real Jetson | Runs on self-hosted Jetson Orin Nano runner |
| Package | Build container image | Multi-arch tag `azaion/autopilot:<branch>-arm64` |
| Sign | Cosign for image; OS signing flow for binary | Tagged builds only |
| Publish | Push image + binary to internal registry | Tagged builds only |
### Pipeline Configuration Notes
- Cache `~/.cargo/registry/`, `~/.cargo/git/`, and `target/` between runs keyed on `Cargo.lock` hash.
- `--features vlm` and the no-feature path are both built and tested to enforce the optionality contract.
- `dev` and `main` branches are protected; force-push forbidden; merges require a green pipeline.
- Benchmark gate is opt-in (manual approval or nightly cron) because it requires a Jetson runner.
## Environment Strategy
| Environment | Purpose | Configuration Notes |
|---|---|---|
| Development (local) | Run autopilot locally against mock detections + mock missions + mock Ground Station; iterate on logic | `docker compose -f docker-compose.yml up`; `config/autopilot.dev.toml`; `RUST_LOG=info,autopilot=debug` |
| Staging | Pre-production: real `../detections`, real `missions` API, real `Ground Station`, but no airframe MAVLink (SITL instead) | `config/autopilot.staging.toml`; secrets via `EnvironmentFile=` |
| Production (airframe) | Native systemd on Jetson Orin Nano per `containerization.md §3` | `/etc/azaion/autopilot/config.toml`; `/etc/systemd/system/autopilot.service`; `/var/lib/autopilot/`; `/run/azaion/in-flight` flight-gate marker |
| CI (Tier-1) | Lint + unit + replay-based integration on amd64 | GitHub-hosted runner; no GPU |
| CI (Tier-2) | Benchmark gate on real Jetson | Self-hosted Jetson Orin Nano Super runner; pinned JetPack + power mode |
### Environment Variables
| Variable | Dev | Staging | Production | Description |
|---|---|---|---|---|
| `AUTOPILOT_CONFIG` | `./config/autopilot.dev.toml` | `/etc/azaion/autopilot/config.toml` | `/etc/azaion/autopilot/config.toml` | Path to TOML config |
| `RUST_LOG` | `info,autopilot=debug` | `info` | `info` | `tracing-subscriber` filter |
| `AUTOPILOT_MISSION_ID` | (per-flight CLI arg) | (per-flight CLI arg) | (per-flight CLI arg) | Active mission UUID; CLI arg, not env |
| `AUTOPILOT_HEALTH_BIND` | `127.0.0.1:8080` | `127.0.0.1:8080` | `127.0.0.1:8080` | HTTP `/health` bind address |
| `AUTOPILOT_VLM_ENABLED` | `false` | `false` (until benchmark passes) | per benchmark | Runtime VLM flag; binary must also build with `--features vlm` |
| `MISSIONS_API_TOKEN` | (mock) | from `EnvironmentFile=` | from `EnvironmentFile=` | Bearer token; never in `config.toml` |
| `GROUND_STATION_TOKEN` | (mock) | from `EnvironmentFile=` | from `EnvironmentFile=` | Bearer / session token |
All non-secret configuration lives in `config.toml` (per `containerization.md §6`). Secrets come from `EnvironmentFile=` on systemd, from compose `secrets:` in containers.
## Database Migration Approach
**Migration tool**: none — autopilot has **no traditional database**.
**Persistence strategy**: the only persisted data is the on-device `mapobjects_store`. Its engine is open (`architecture.md §8 Q3`); the bootstrap default is **in-memory + snapshot to `/var/lib/autopilot/mapobjects/`** (file-backed, no schema migrations). When Q3 resolves toward SQLite + H3 or another engine, the `mapobjects_store` crate's engine module is swapped without changing its public API. The central `missions` API owns its own Postgres schema (per `architecture.md §7.13`) — autopilot does NOT migrate central tables.
### Initial Persisted Surface
| Subsystem | What is persisted | Where | Format |
|---|---|---|---|
| `mapobjects_store` | `current_state`, `pending_observations`, `pending_ignored`, `sync_state` | `/var/lib/autopilot/mapobjects/` | engine-defined; default = JSON snapshots + append-only log |
| `operator_bridge` audit log | accepted/rejected `OperatorCommand` envelopes | `/var/lib/autopilot/audit/` | newline-delimited JSON |
| `mission_client` deferred uploads | post-flight push payload on push failure | `/var/lib/autopilot/pending_pushes/` | JSON files keyed by mission ID |
Disk quota for `/var/lib/autopilot/` is configured in `config.toml`; persistent-store-full at pre-flight BIT is a takeoff blocker (per `architecture.md §5`).
## Test Structure
```
crates/<component>/
└── tests/ # crate-level integration tests; per-crate
└── <scenario>.rs
tests/
└── e2e/ # workspace-level end-to-end (uses docker-compose.test.yml)
├── sitl_conformance.rs # SITL gate per ci_cd_pipeline.md §5
├── geofence_inclusion.rs
├── geofence_exclusion.rs # explicit regression vs earlier silent-ignore behaviour
├── lost_link_failsafe.rs
└── operator_command_replay.rs
fixtures/
├── rtsp/<clip>.h264
├── mavlink/<replay>.tlog
├── missions/<mission>.json
└── detections/<deterministic>.json
benches/
├── tier1_latency.rs # benchmark-gate harness
├── tier2_latency.rs
├── gimbal_zoom.rs
└── movement_fpr.rs # per-zoom-band FPR replay
```
### Test Configuration Notes
- **Unit tests** live alongside each component's source in `#[cfg(test)] mod tests { ... }` within `src/` files. They MUST run in <5 s on developer workstation; no network, no Docker.
- **Crate-level integration tests** live in `crates/<component>/tests/`. They may use fixtures from `fixtures/` but MUST NOT cross component boundaries — that's what workspace e2e is for.
- **Workspace e2e** in `tests/e2e/` exercises the full binary against a docker-compose-managed stack (ArduPilot SITL, mock missions API, mock detections gRPC, replay RTSP).
- **Replay-driven debugging**: all non-trivial decisions are reconstructable from logs + size-capped raw inputs (per `observability.md §6`). Replay fixtures are the foundation of regression tests.
- **Test runner**: `cargo test --workspace` for unit + integration; `docker compose -f docker-compose.test.yml up --abort-on-container-exit` for e2e; `cargo bench` (or `criterion`) for benchmark-gate measurements.
- **Mock-data discipline**: mocks live in `tests/` directories only — never in production crates (per `coderule.mdc`).
## Implementation Order
| Order | Component | Reason |
|---|---|---|
| 1 | `shared` (models + config + error + health + observability + clock) | Every other crate depends on it; nothing depends on it. Must land first. |
| 2 | `mavlink_layer` | Self-contained transport; required by `mission_executor` and `telemetry_stream`; SITL conformance lands the first hard gate early. |
| 3 | `mission_client` | Self-contained REST client; required by `mission_executor` and `mapobjects_store` sync. |
| 4 | `mission_executor` | Combines `mavlink_layer` + `mission_client` + geofence/failsafe logic; gates takeoff via BIT. |
| 5 | `gimbal_controller` | Self-contained A40 UDP driver; required by `scan_controller`. |
| 6 | `frame_ingest` | RTSP decoder; required by all perception crates. |
| 7 | `detection_client` | gRPC client to `../detections`; required by `scan_controller` and `telemetry_stream`. |
| 8 | `movement_detector` | Depends on `frame_ingest` + `GimbalState`; standalone otherwise. |
| 9 | `mapobjects_store` | Engine choice may be deferred; default in-memory+snapshot unblocks `scan_controller`. |
| 10 | `semantic_analyzer` | Tier 2; depends on `Frame` + `Detection`. |
| 11 | `vlm_client` | Optional; default impl returns `vlm_disabled`. Real IPC implementation can land later. |
| 12 | `telemetry_stream` | Pure egress; ready once `frame_ingest`, `detection_client`, `mavlink_layer` exist. |
| 13 | `operator_bridge` | Depends on `telemetry_stream` + `mapobjects_store`; envelope auth scheme is Q9-stubbed. |
| 14 | `scan_controller` | Sits on top of everything in Perception + Action; lands last. |
| 15 | `autopilot` binary (composition root) | Wires every component handle; runs the actor topology. |
## Acceptance Criteria
**AC-1: Workspace scaffolded**
Given the structure plan above
When the implementer executes this task
Then `cargo metadata` lists all 14 crates (`shared`, `autopilot`, and 12 components — `vlm_client` is the 13th component crate but listed under perception above) and `cargo check --workspace` succeeds with no compile errors.
**AC-2: Stub tests runnable**
Given the scaffolded workspace
When `cargo test --workspace` runs on a developer workstation (no Docker, no GPU)
Then every crate's stub test (e.g. `it_compiles()`) passes within 5 seconds total.
**AC-3: CI pipeline configured**
Given the scaffolded workspace
When the Woodpecker pipeline runs on a feature branch push
Then `fetch → lint → unit-test → build-arm64 → build-no-vlm → integration-test → sitl-conformance` all complete successfully on a known-good baseline commit.
**AC-4: Dev compose boots**
Given `docker-compose.yml`
When `docker compose -f docker-compose.yml up -d` runs on a fresh workstation
Then the autopilot container starts, the `/health` endpoint returns HTTP 200 with `status: green | yellow` (red is acceptable here only for components without a mock target), and the mock detections + mock missions services are reachable.
**AC-5: Blackbox compose boots with SITL**
Given `docker-compose.test.yml`
When `docker compose -f docker-compose.test.yml up --abort-on-container-exit` runs
Then ArduPilot SITL + autopilot + mock detections + replay RTSP all start, and the SITL conformance e2e test exits 0.
**AC-6: Optionality contract enforced**
Given the scaffolded workspace
When `cargo build --workspace --no-default-features` runs
Then the binary builds and links without the `vlm` feature; `cargo test --workspace --no-default-features` passes; the `VlmProvider` default impl returns `VlmAssessment{status=vlm_disabled}`.
**AC-7: Cross-compile target ready**
Given `.cargo/config.toml` configured for `aarch64-unknown-linux-gnu`
When `cross build --target aarch64-unknown-linux-gnu --release` (or `cargo zigbuild` equivalent) runs in CI
Then an aarch64 binary is produced and stored as an artifact.
**AC-8: Flight-gate marker wiring exists**
Given `deploy/systemd/autopilot.service`
When systemd parses the unit
Then `ExecStartPre` asserts `/run/azaion/in-flight` is created and `ExecStopPost` removes it (per `containerization.md §3` and the suite-level flight-gate convention).
**AC-9: Observability scaffold initialised**
Given the autopilot binary
When it starts
Then `tracing-subscriber` emits JSON-formatted logs to stdout with the per-line fields enumerated in `observability.md §2` (`ts`, `ts_mono_ns`, `level`, `target`, `event`), and the `/health` endpoint returns the per-component breakdown documented in `containerization.md §7`.
**AC-10: Persistent state directory created**
Given `/var/lib/autopilot/` (or its container-mounted equivalent)
When autopilot starts in dev or prod
Then the binary creates `mapobjects/`, `audit/`, and `pending_pushes/` subdirectories with the owning user, fails closed if any directory cannot be created, and surfaces the failure to `/health` (red on `mapobjects_store`).