mirror of
https://github.com/azaion/autopilot.git
synced 2026-06-21 17:01:10 +00:00
[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:
@@ -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`).
|
||||
Reference in New Issue
Block a user