# 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//`). 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` | 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`, `health()` | `scan_controller`, `movement_detector`, `telemetry_stream` | | `movement_detector` | `MovementDetectorHandle::candidates() -> CandidateStream`, `health()` | `scan_controller` | | `semantic_analyzer` | `SemanticAnalyzerHandle::analyze(Roi) -> Result`, `health()` | `scan_controller` | | `vlm_client` (trait) | `VlmProvider::assess(Roi) -> Result` (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:-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// └── tests/ # crate-level integration tests; per-crate └── .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/.h264 ├── mavlink/.tlog ├── missions/.json └── detections/.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//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`).