Files
autopilot/_docs/02_tasks/done/AZ-640_initial_structure.md
T
Oleksandr Bezdieniezhnykh a1ce3a6903
ci/woodpecker/push/build-arm Pipeline failed
[AZ-640] Bootstrap Rust workspace, CI/Docker, observability scaffold
Lands the first task of the implementation epic AZ-626: a cargo workspace
with 14 crates (shared + autopilot binary + 12 component crates), a
multi-stage Dockerfile + dev/test compose stacks, a Woodpecker CI pipeline,
the on-airframe systemd unit with flight-gate wiring, three environment
TOML configs, and the canonical entity catalogue from data_model.md as
`shared::models`.

Per-AC verification (full detail in
_docs/03_implementation/batch_01_cycle1_report.md):

- AC-1 cargo check --workspace clean
- AC-2 cargo test --workspace passes; per-crate it_compiles() <0.01 s
- AC-6 cargo build/test --no-default-features clean; VlmClient default
       impl returns VlmAssessment::disabled()
- AC-9 tracing-subscriber emits JSON logs with ts/level/target/fields
- AC-10 runtime::ensure_state_directories creates mapobjects/, audit/,
        pending_pushes/ under storage.state_dir

Deferred to external infra (artifacts written, verification re-runs in CI
and in downstream tasks):
- AC-3 Woodpecker runner; CI yml in place
- AC-4 docker-compose mocks land with AZ-660/AZ-644/AZ-675
- AC-5 SITL conformance lands with AZ-641/AZ-648/AZ-652
- AC-7 aarch64 cross-compile via cargo-zigbuild stage
- AC-8 systemd unit (Linux + systemd host)

Layering invariants from module-layout.md hold: shared (L1) imports
nothing; Layer 2 actor crates import only shared; Layer 3 coordinators
(operator_bridge, mission_executor) import only their documented Layer 2
deps; Layer 4 (scan_controller) imports its documented Layer 2 + Layer 3
deps; the autopilot binary (L5) is the only consumer of every component.

cargo fmt --all --check + cargo clippy --all-targets -- -D warnings both
clean. Jira AZ-640 transitioned to In Progress at the start of this batch;
the matching In Testing transition follows this commit.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 11:52:40 +03:00

26 KiB

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).