feat(simulation): establish polygon S0 qualification

This commit is contained in:
DCCONSTRUCTIONS 2026-07-24 15:29:08 +03:00
parent 7fba39a629
commit 81761067e9
11 changed files with 2318 additions and 0 deletions

View File

@ -121,6 +121,28 @@ performance, direct worker transport and bounded live fan-out remain open. See
[ADR 0014](docs/adr/0014-bounded-external-perception-worker.md)
and the [external worker contract](docs/10_EXTERNAL_PERCEPTION_WORKER.md).
## Simulation Polygon
Mission Core now has a parallel Polygon product branch for reproducible
autonomy qualification. Mission Core owns scenarios, qualification-run
lifecycle, authority, canonical contracts, provenance and reports; Gazebo, PX4
SITL, ROS 2, Nav2 and viewers remain replaceable providers. Simulation, replay,
digital twin, HIL and physical shadow keep distinct causal run kinds.
The current implementation is SIM S0 preflight only. Its strict candidate
profile fixes D-only worker placement, loopback ports, process ownership,
Gazebo simulation time and disabled real/direct actuator authority. The doctor
does not install or launch anything and cannot report `GO` without
digest-bound target-worker evidence:
```bash
uv run missioncore-sim s0 doctor --json
```
See the [Polygon product/SRS](docs/12_SIMULATION_POLYGON_PRODUCT_AND_SRS.md),
[ADR 0015](docs/adr/0015-simulation-polygon-qualification-boundary.md) and the
[SIM S0 worker runbook](docs/runbooks/SIM_S0_AI_WORKER.md).
## Mission Core Control Station and visualization adapters
The browser application is the universal Mission Core Control Station rather than
@ -219,6 +241,10 @@ standalone release install. The frontend consumes sibling `file:` packages from
revision or content hash. Publishing/vendoring those packages or enforcing an
immutable donor revision remains a packaging and CI prerequisite.
Polygon is planned as a seventh section, but UI delivery is gated on the
accepted clock/frame/authority contracts and a proven S1 backend lifecycle with
persisted run history. It is not present in the current shell.
The Observation spatial workspace embeds the open-source Rerun Web Viewer
inside the Mission Core shell. It can open a compatible RRD over same-origin
HTTP or a Rerun gRPC/proxy source such as

View File

@ -24,6 +24,7 @@ Each gate produces evidence and an explicit GO, PAUSE or BLOCKED result.
| Plugin isolation | GO (laboratory control plane) — vendor backend/frontend and optional scene controls are plugin-owned; manifest/runtime descriptor parity, versioned handshake, lifecycle health and transport correlation fail closed while execution remains in-process |
| K1 application control | GO (physical staged cycle) — after fixing the PCAP-proven `sint64` time field, one explicit UI launch completed all 14 canonical operations on one control session, reached live `SCANNING + project + init_ready`, displayed real points, then one explicit STOP returned K1 to unbound `READY`. No retry or fallback command was sent. Native-project reuse through LixelGO/USB remains an independent verification |
| Stage 8 product storage | PAUSE — retention, replication, encryption, capacity monitoring and long-run browser/WASM stress remain deployment gates |
| Simulation Polygon | STARTED, NOT ACCEPTED — product/SRS and ADR 0015 define a separate qualification boundary; SIM S0 has a strict candidate D-only/version/clock/port/process profile and read-only doctor. No target-worker evidence, rover run, navigation/safety acceptance or real actuator authority exists |
USB project copying remains optional ground truth rather than a blocker for the
now-verified network path. Owner-operated LixelGO traffic verifies the MQTT
@ -121,6 +122,32 @@ control is plugin-owned. A plugin-commanded acquisition sends one canonical STOP
and seals locally after protocol-reported standby; an operator-manual acquisition
still stops and seals only local reception and reports scanner state as unknown.
## Parallel branch — Simulation Polygon
The Polygon branch follows
[`docs/12_SIMULATION_POLYGON_PRODUCT_AND_SRS.md`](12_SIMULATION_POLYGON_PRODUCT_AND_SRS.md)
and
[`ADR 0015`](adr/0015-simulation-polygon-qualification-boundary.md).
It does not reorder or weaken the K1 physical-evidence gates in this document.
Its first gate is SIM S0:
- candidate Windows 11/WSL2 Ubuntu 24.04 worker profile;
- all mutable runtime bytes physically on D;
- ROS 2 Jazzy, Gazebo Harmonic, PX4 v1.17.0, matching `px4_msgs`,
Micro XRCE-DDS and Nav2 compatibility;
- authoritative Gazebo `/clock`;
- loopback port and deterministic process ownership;
- target-worker rover/telemetry/pause/step/stop/resource evidence;
- explicit `GO`, `INCOMPLETE` or `BLOCKED` result.
The repository doctor is side-effect free:
```text
uv run missioncore-sim s0 doctor --json
```
Without admitted target-worker evidence its correct result is `INCOMPLETE`.
## Stage 0 — repository and host baseline
Deliverables:

View File

@ -0,0 +1,535 @@
# Mission Core Polygon: product definition and system requirements
Status: canonical working SRS, 2026-07-24.
Ops source of truth:
- [MISSIONCOR-38](https://ops.nodedc.ru/nodedc/browse/MISSIONCOR-38) is the
product epic and original simulation-branch scope.
- [MISSIONCOR-39](https://ops.nodedc.ru/nodedc/browse/MISSIONCOR-39) is the
canonical, changeable architecture plan.
- [MISSIONCOR-40](https://ops.nodedc.ru/nodedc/browse/MISSIONCOR-40) is the
active SIM S0 implementation and qualification gate.
This document and ADR 0015 are the repository truth. A material change to run
kinds, authority, clocks, frames, source-of-record, provider boundaries or phase
gates must update the ADR/SRS and MISSIONCOR-39 together.
## 1. Product thesis
Polygon is Mission Core's autonomy qualification and assurance layer. It is not
a simulator embedded for visual effect and it is not a second observation
viewer.
Polygon makes one product promise:
> Given a versioned scenario and qualification profile, Mission Core can run or
> replay the autonomy stack, preserve exact provenance and evidence, explain
> each control/safety decision and compare outcomes without granting undeclared
> authority.
The product value appears in increments:
| Gate | Value |
| --- | --- |
| P0/S0 | Makes the stack installable, inspectable and reproducible rather than an ad hoc lab |
| S1 | Proves Mission Core can own a complete virtual rover run lifecycle |
| S2 | Produces the first useful navigation baseline and comparable safety/latency evidence |
| S3 | Connects Mission Core perception to a closed-loop consumer and exposes product differentiation |
| S4 | Makes recorded real data useful for regression and shadow-policy analysis |
| S5/S6 | Reduces risk before a real vehicle, without claiming real-world safety certification |
The first product-useful baseline is P0 through S2. The current estimate is
2746 engineer-days, excluding waits for hardware, external accounts or a
resolved worker-access blocker.
## 2. Current truth
The repository's proven vertical remains K1 observation, durable archive,
recorded perception and diagnostic motion evidence through LAB E26. It has not
accepted navigation behavior, safety behavior or real actuator control.
Polygon is now a parallel product branch. As of this document:
- P0 architecture and SRS are being committed;
- SIM S0 has a strict candidate qualification profile and read-only doctor;
- no target-worker S0 evidence has been admitted;
- PX4/Gazebo/ROS 2/Nav2 have not been installed or accepted by this repository;
- no stock rover run has been accepted;
- `actuator_authority=false`;
- `navigation_or_safety_accepted=false`.
The doctor must report `INCOMPLETE` until exact worker evidence is supplied. A
repository manifest is never enough for a `GO`.
## 3. Goals
### 3.1 Product goals
- Turn perception and planning work into measurable sense-plan-act experiments.
- Compare standard and Mission Core planners against the same versioned inputs.
- Preserve failures, limitations and uncertainty as first-class evidence.
- Reuse the same scenario/run/report concepts across simulation, replay, digital
twin, controller-in-loop, HIL and physical shadow.
- Keep physics, autopilot, middleware, planner and viewer replaceable.
- Create a controlled path toward a future trike without silently widening
authority.
### 3.2 Engineering goals
- Deterministic lifecycle ownership with no orphan simulator processes.
- Exact version, environment, input and artifact provenance.
- Explicit clock-domain and coordinate-frame conversions.
- Fail-closed setpoint validity, TTL, heartbeat, watchdog and loss behavior.
- Append-only run history and digest-bound source/derived artifacts.
- Side-effect-free infrastructure diagnosis before installation.
## 4. Non-goals
The first delivery does not:
- grant real actuator authority;
- expose PX4 directly to the browser;
- use direct actuator setpoints;
- certify Nav2, PX4 or Mission Core as a real-time safety system;
- treat replay as closed-loop evidence;
- import all 300 BARN worlds before a stock-world baseline;
- download complete CODa, JRDB or NCLT collections;
- build a custom physics engine;
- make `px4-ros2-interface-lib` mandatory for S1;
- overload the observation-session model for qualification runs;
- implement a top-level UI before the backend S1 gate.
## 5. Product and system boundaries
### 5.1 Mission Core ownership
Mission Core owns:
- scenario definitions and profile selection;
- qualification-run identity and lifecycle;
- command authority and safety policy;
- canonical state, setpoint, decision and event contracts;
- orchestration of replaceable providers;
- evidence admission, provenance and immutable history;
- evaluation, comparison and qualification reports.
### 5.2 Provider ownership
Providers remain replaceable:
- Gazebo owns simulated world state, sensors, contacts and physics.
- PX4 SITL owns rover control loops, constraints, offboard lifecycle and
autopilot failsafe.
- ROS 2 transports typed telemetry and commands.
- Nav2 supplies the first mature planning/costmap/collision-checking baseline.
- Rerun or another viewer presents derived evidence and never becomes a queue,
orchestrator or source-of-record.
- Dataset adapters expose recorded streams but never pretend alternative
commands changed future recorded observations.
### 5.3 Neighboring Mission Core contexts
- Observation owns real and recorded sensor streams plus perception layers.
- Polygon owns scenarios, qualification runs, decisions, simulated state and
comparisons.
- Missions owns goals and routes that may later target a virtual or real
vehicle.
- Data owns source-of-record, derived artifacts, retention and report access.
Observation `SessionStatus` and its modality model are not extended to describe
simulation. Shared UI components require an explicit data-source contract.
## 6. Target architecture
```text
Mission Core UI
|
Simulation API
|
Simulation Orchestrator
+---- scenario/profile registry
+---- run state machine and authority gate
+---- clock, namespace, port and process ownership
+---- artifact/provenance store and evaluator
|
+---- Gazebo adapter ---- Gazebo world/physics/sensors
+---- PX4 adapter ------- PX4 SITL rover controller/failsafe
+---- ROS 2 adapter ----- Micro XRCE-DDS and canonical topics
+---- planner adapter --- Nav2 or Mission Core planner
+---- viewer adapter ---- canonical state/derived report
```
Gazebo and PX4 never run inside the web process. The browser never owns process
lifecycle or command authority. Losing the browser cannot terminate the only
copy of run state and cannot bypass the server-side authority gate.
## 7. Canonical domain model
### 7.1 Versioned inputs
`ScenarioDefinition` contains identifiers and references for:
- world and deterministic actor placement;
- vehicle profile;
- sensor profile;
- route/goal;
- planner profile;
- fault profile;
- metric profile;
- seed and reproducibility tier.
`VehicleProfile`, `SensorProfile`, `PlannerProfile`, `FaultProfile` and
`MetricProfile` are independently versioned. A run stores the exact resolved
generation and hashes, not only mutable names.
### 7.2 QualificationRun
`QualificationRun` is the umbrella aggregate. `SimulationRun` is not used as
the common type because replay, HIL and physical shadow are not simulations.
Required identity:
- immutable run ID and episode ID;
- run kind;
- scenario/profile generations and hashes;
- Mission Core commit;
- provider versions, commits or image digests;
- host and resource profile;
- seed and reproducibility tier;
- authority profile;
- start/end UTC provenance;
- authoritative clock domain.
Reset creates a new episode/run identity. It never reuses a completed identity
or overwrites artifacts.
### 7.3 Run kinds
| Kind | Commands affect future inputs | Actuator authority |
| --- | --- | --- |
| `simulation_closed_loop` | Yes, through virtual physics | Virtual only |
| `replay_shadow` | No | None |
| `digital_twin_closed_loop` | Yes, through an attested twin | Virtual only |
| `controller_in_loop` | Yes, through a bounded controller interface | Lab-specific |
| `hil` | Yes, with hardware in the loop | Separate gate |
| `physical_shadow` | No | None |
No result may be promoted from one kind to another by changing report wording.
### 7.4 Canonical streams
- `VehicleState`: pose, twist, acceleration, steering/wheel state, estimator
state and source frames.
- `PerceptionWorldState`: occupancy, objects, motion, confidence, unknown and
uncertainty.
- `ControlSetpoint`: profile, sequence, issued time, validity deadline,
speed/steering or speed/yaw-rate and authority scope.
- `SafetyDecision`: allow/slow/stop, reason, active constraint, TTC/clearance
evidence and input generation.
- `QualificationEvent`: lifecycle, collision/contact, reset, deadline miss,
data loss, watchdog, failsafe and evaluator events.
- `QualificationReport`: verdict, metrics, violations, limitations and evidence
links.
## 8. Lifecycle requirements
The minimum lifecycle is:
```text
admitted -> starting -> running <-> paused -> stopping -> completed
| |
+--------------------+-> failed
```
Cancellation is a requested transition, not evidence that processes stopped.
A run reaches a terminal state only after:
- child-process ownership is reconciled;
- stop/failsafe outcome is captured;
- source artifacts are sealed;
- derived indexes are generated or explicitly marked absent;
- the report records the final limitations.
Process shutdown order is explicit and deterministic. Orchestrator shutdown is
last. An orphan process is an S0/S1 failure.
## 9. Clock contract
For `simulation_closed_loop`:
- Gazebo `/clock` is authoritative.
- ROS 2 consumers use `use_sim_time=true`.
- PX4 uXRCE-DDS time synchronization is disabled when Gazebo time is used.
- Canonical timestamps are integer nanoseconds and include their clock domain.
- UTC is provenance only.
- TTL, deadline and watchdog calculations declare whether they use simulation
or monotonic host time.
- Pause, resume, single-step and real-time-factor changes are qualification
cases, not UI-only behavior.
Wall clock must not appear implicitly in planner or safety calculations.
## 10. Coordinate-frame contract
Mission Core navigation uses:
- `map` and `odom`: ENU;
- `base_link`: FLU.
PX4 uses:
- world/local: NED;
- body: FRD.
All PX4 conversion happens at one adapter boundary. Pose, vector, yaw, yaw-rate
and covariance transformations require golden tests. Every canonical message
identifies source and target frame. A frame mismatch fails closed and cannot be
reclassified as planner noise.
## 11. Authority and safety requirements
Until a new physical-control card and formal safety gate:
- `simulation_or_shadow_only=true`;
- `actuator_authority=false`;
- `navigation_or_safety_accepted=false`;
- direct actuator setpoints are rejected;
- supported S1 command profiles are `rover-speed-steering/v1` and
`rover-speed-yaw-rate/v1`;
- each command carries a monotonic sequence and explicit expiry;
- heartbeat loss, TTL expiry, invalid state, planner timeout or link loss causes
a stop/failsafe transition;
- unknown occupancy never becomes free space by omission;
- ground truth is unavailable to the planner and perception runtime;
- ground truth is separately available to the evaluator.
Nav2 Collision Monitor is an additional software safety layer. It is not a
hard-real-time certified safety controller.
## 12. Evidence and data requirements
### 12.1 Source-of-record
The minimum source set is:
- exact scenario and qualification profiles;
- stack/version lock;
- run manifest and authority profile;
- PX4 ULog;
- Gazebo log or equivalent world-state/contact evidence;
- rosbag2/MCAP for admitted canonical streams;
- factual host/resource and process-lifecycle evidence.
### 12.2 Derived artifacts
Derived data includes:
- normalized metrics;
- evaluator outputs;
- comparison tables;
- Rerun recording;
- qualification report.
Presentation state is not source evidence. Derived artifacts are rebuildable and
must retain input hashes. Source and derived generations are append-only.
### 12.3 Required metrics
Each applicable run reports:
- completion and terminal reason;
- collision/contact count and minimum clearance;
- path length, elapsed simulation time and efficiency;
- route/pose error;
- speed/steering tracking;
- stop time and distance;
- TTC and constraint activation;
- replans and oscillation;
- dropped/late messages and deadline misses;
- stage latency p50/p95/max and queue depth;
- real-time factor;
- CPU, GPU, RAM, VRAM and disk usage.
Inapplicable metrics are explicit, not silently zero.
## 13. Reproducibility
- R0: exact inputs and versions are retained; outcome may vary.
- R1: the same inputs/seed/version lock reproduce the same verdict within
declared tolerances.
- R2: defined metrics remain within accepted tolerance bands.
Bitwise-identical physics is not promised. Comparison is rejected when scenario
schema, metric profile, provider generation or reproducibility tier are
incompatible.
## 14. Upstream baseline
The S0 candidate line is:
- Windows 11 AI worker;
- WSL2 Ubuntu 24.04;
- ROS 2 Jazzy;
- Gazebo Harmonic;
- PX4 Autopilot v1.17.0;
- matching `px4_msgs` v1.17.0 generation;
- Micro XRCE-DDS Agent v2.4.3 candidate;
- Nav2 Jazzy;
- PX4 Gazebo stock rover models.
The exact source commits already resolved in the candidate profile do not become
accepted merely because they are present. ROS/Gazebo package versions and every
candidate build remain pending target-worker qualification.
S1 uses direct exact `px4_msgs` integration. The experimental
`px4-ros2-interface-lib` is evaluated later and is not on the S1 critical path.
## 15. Storage and runtime placement
All mutable worker bytes must be physically placed under
`D:\NDC_MISSIONCORE`, including:
- WSL distribution/VHD;
- Docker data and images;
- sources and build products;
- package/model caches;
- runtime and logs;
- bags, ULog and Gazebo evidence;
- datasets and reports.
The WSL view is `/mnt/d/NDC_MISSIONCORE`. C-drive placement is a blocking
failure. S0 requires at least 300 GiB free before installation and a stop-below
threshold of 200 GiB. Full external datasets are out of S0 scope.
## 16. Delivery plan
| Phase | Estimate | Exit condition |
| --- | ---: | --- |
| P0 | 24 days | Product thesis, SRS, ADR and canonical contracts accepted |
| S0 | 35 days | Target compatibility, D-only runtime, exact pins, clock/process/resource evidence |
| S1 | 712 days | Stock Ackermann and Differential lifecycle/control/failsafe evidence |
| S2 | 1525 days | LiDAR/odometry/Nav2 baseline, evaluator and obstacle scenarios |
| S2B | 58 days | Representative 30-world BARN pilot |
| S3 | 2035 days | K1-like virtual sensors and Mission Core perception closed loop |
| S4 | 1020 days | Canonical replay/shadow adapters and reports |
| S5 | 2040 days | Attested RAVNOVES00 digital twin |
| S6 | 2040+ days | Trike model, controller-in-loop/HIL and physical-shadow gate |
Implementation cards follow this order:
1. Product SRS and ADR.
2. D-only inventory and stack lock.
3. Clock, frame and authority contracts.
4. Orchestrator lifecycle and artifact store.
5. PX4 stock rover control and failsafe.
6. Nav2 adapter and truth baseline.
7. Evaluator, report and BARN pilot.
8. Virtual K1 and perception.
9. Replay/shadow datasets.
10. RAVNOVES00 twin.
11. Trike, HIL and safety gate.
## 17. SIM S0 acceptance
SIM S0 requires all of the following factual evidence:
- worker inventory;
- physical D-only placement, including WSL and Docker VHD;
- exact component/package pins and licenses;
- one stock PX4 rover launch;
- ROS 2 telemetry;
- authoritative simulation clock;
- pause, resume, single-step and speed-factor behavior;
- clean stop with no orphan processes;
- 1× resource baseline;
- 2× resource baseline;
- explicit `GO`, `PAUSE` or `BLOCKED` report.
The repository profile and doctor implement preflight and evidence admission.
They do not install packages, start processes, open ports or write a target
report.
Run the local read-only preflight:
```bash
uv run missioncore-sim s0 doctor --json
```
An optional evidence manifest uses
`missioncore.simulation-s0-evidence/v1`. Each required claim points to a
confined regular artifact and supplies its SHA-256. The profile SHA-256 must
match the exact candidate generation.
## 18. S1 and S2 acceptance outline
### S1
- Ackermann primary and Differential reference launch from the orchestrator.
- Start, pause, resume, step, reset and stop are server-owned.
- Straight, turn, reverse, deceleration and emergency-stop cases pass.
- TTL, heartbeat, offboard loss and link loss produce a captured safe outcome.
- Pose, speed, steering/controller state and failsafe telemetry are retained.
- Reset creates a new run/episode.
- A repeated seed produces the declared R1 verdict tolerance.
K1, Nav2 and top-level UI do not block S1 backend acceptance.
### S2
- Gazebo LiDAR/odometry feed the ROS 2/Nav2 baseline.
- Ackermann uses MPPI plus Smac Hybrid/Lattice or an explicitly justified
equivalent.
- Static wall, corridor, bypass, narrow passage, no-data, planner timeout and
control-loss scenarios produce comparable reports.
- PX4 waypoint following without obstacle avoidance and Nav2 are measured on
the same profiles.
- A 30-world BARN pilot follows stock-world acceptance; only geometry, maps,
paths and difficulty are imported from the legacy runtime.
## 19. Replay and dataset policy
Recorded datasets support detector, segmentation, tracking, occupancy and
shadow-policy regression. They do not react to alternative commands and cannot
prove a closed-loop avoidance maneuver.
- CODa and JRDB non-commercial/share-alike restrictions prohibit treating them
as unrestricted commercial runtime dependencies.
- NCLT is large and must be admitted by a size/license manifest.
- SCAND supplies social-navigation demonstrations but remains replay evidence.
- RAVNOVES00 first enters as canonical replay/shadow; only S5 may turn an
attested reconstruction into a digital-twin world.
Unknown or unobserved space remains unknown/occupied according to the selected
safety policy.
## 20. UI gate
Polygon is planned as the seventh top-level bounded context:
```text
Center | Fleet | Observation | Polygon | Missions | Data | System
```
Top-level UI implementation begins only after:
1. this SRS and the clock/frame/authority contracts are accepted;
2. S1 backend lifecycle is proven;
3. run history is persisted independently of a browser session.
The future UI may present Scenarios, Run, Control, History, Compare and Report.
It cannot own provider processes, bypass authority checks or write directly to
PX4.
## 21. Change control
A material change requires:
1. an ADR/SRS diff;
2. an update to MISSIONCOR-39;
3. migration and compatibility notes;
4. new or updated tests;
5. fresh evidence before a previously accepted gate remains accepted.
No checkbox is closed from intent alone. A commit plus factual validation or
admitted evidence is required.

View File

@ -0,0 +1,82 @@
# ADR 0015: Simulation Polygon as a qualification boundary
## Status
Accepted for architecture and SIM S0 implementation on 2026-07-24.
Navigation behavior, safety behavior and real actuator authority are not
accepted.
## Context
Mission Core has proven a K1 observation/archive/perception vertical, but the
perception outputs do not yet have a closed-loop consumer. Adding Gazebo, PX4,
ROS 2 and Nav2 directly to the web process or modeling every experiment as an
observation session would blur product ownership, lifecycle, evidence and
authority.
Replay, physics simulation, a digital twin, HIL and real shadow also have
different causal meaning. A recorded stream cannot react to a hypothetical
command, while a closed-loop world can. Calling both `SimulationRun` would make
reports easy to misuse.
## Decision
1. Polygon is a first-class Mission Core bounded context for autonomy
qualification and assurance.
2. Mission Core owns scenarios, qualification-run lifecycle, authority policy,
canonical contracts, provenance, evidence admission, evaluation and reports.
3. Gazebo, PX4 SITL, ROS 2, Nav2, Rerun and datasets are replaceable providers.
4. `QualificationRun` is the umbrella aggregate. Its kinds explicitly separate
`simulation_closed_loop`, `replay_shadow`,
`digital_twin_closed_loop`, `controller_in_loop`, `hil` and
`physical_shadow`.
5. Simulation Orchestrator is the sole process-lifecycle owner. Gazebo and PX4
do not run inside the Mission Core web process, and the browser has no direct
PX4 channel.
6. Gazebo `/clock` is authoritative in closed-loop simulation. ROS 2 uses
simulated time and PX4 uXRCE-DDS time synchronization is disabled for this
profile.
7. Mission Core uses ENU/FLU. PX4 uses NED/FRD. Exactly one adapter boundary
converts frames and requires golden tests.
8. The first command boundary permits only rover speed+steering and
speed+yaw-rate profiles with sequence, TTL, heartbeat, watchdog and
stop-on-loss behavior. Direct actuator setpoints are rejected.
9. Source-of-record, derived artifacts and presentation state remain separate.
Reset creates a new immutable run/episode.
10. Ground truth is evaluator-only and is not visible to perception or planning.
11. SIM S0 targets the reviewed Windows 11/WSL2 AI worker with all mutable bytes
physically under `D:\NDC_MISSIONCORE`.
12. S1 uses exact `px4_msgs` directly. The experimental
`px4-ros2-interface-lib` is not a mandatory S1 dependency.
13. A top-level Polygon UI is gated on accepted contracts and a proven S1
backend lifecycle with persisted run history.
14. Real actuator authority requires a new decision and physical safety gate.
## Consequences
- Perception can be measured by downstream decisions without granting it hidden
behavior authority.
- Standard planners and future Mission Core planners can be compared on the
same scenario and metric profiles.
- Replay cannot be presented as closed-loop safety evidence.
- Provider upgrades require explicit compatibility and evidence refresh.
- More up-front domain and artifact work is required before a visual demo.
- The AI worker must satisfy D-only, resource and coexistence constraints before
installation is accepted.
- Simulation and HIL reduce risk but do not certify the real trike.
## First implementation
The first implementation is deliberately read-only:
- `simulation/s0/qualification-profile.yaml` is the candidate stack,
storage, clock, authority, port and process contract.
- `k1link.simulation.s0` strictly validates that contract and optional
digest-bound worker evidence.
- `missioncore-sim s0 doctor` inspects the current host without installing,
starting or modifying anything.
- MISSIONCOR-40 tracks factual worker qualification.
The doctor reports `INCOMPLETE` until it is running on the reviewed target with
resolved/accepted pins and all required evidence. It cannot infer `GO` from the
repository state.

View File

@ -0,0 +1,112 @@
# SIM S0 AI worker runbook
This runbook is for MISSIONCOR-40. It does not authorize system installation,
real vehicle control or a `GO` verdict by itself.
## Safety and placement
- Target: reviewed Windows 11 worker with WSL2 Ubuntu 24.04.
- All mutable bytes must be under `D:\NDC_MISSIONCORE`.
- Do not place a WSL distribution, Docker VHD, source, build, cache, log, bag,
ULog, Gazebo artifact or dataset on C.
- Do not expose S0 services beyond loopback.
- Do not download full external datasets.
- Keep `actuator_authority=false`.
## Repository preflight
From the repository-local Python 3.12 environment:
```bash
uv sync --frozen --group dev
uv run missioncore-sim s0 doctor --json
```
Before target evidence exists, the expected verdict is `INCOMPLETE`.
## Required target inventory
Record a redacted artifact for each of:
1. Windows release, WSL version, Ubuntu version and architecture.
2. GPU/driver, CPU, RAM and VRAM.
3. Physical locations of the WSL distribution and Docker data/VHD.
4. D total/free bytes before installation.
5. Existing Docker images, WSL distributions and simulation-related processes.
6. Existing listeners and the candidate port registry.
7. Existing Frigate/Ollama/Triton resource use and whether S0 is coexistence or
exclusive-profile testing.
Do not record credentials, tokens or private network secrets.
## Candidate stack
Use `simulation/s0/qualification-profile.yaml` as the input. Resolve and record:
- exact ROS 2 Jazzy package versions;
- exact Gazebo Harmonic package versions;
- PX4 v1.17.0 commit;
- matching `px4_msgs` commit;
- Micro XRCE-DDS Agent commit/version;
- Nav2 Jazzy commit/package versions;
- PX4 Gazebo model commit;
- licenses and image digests where containers are used.
Change every component to `accepted` only after target build/runtime evidence
exists. Updating a pin changes the profile SHA-256 and invalidates evidence for
the prior generation.
## Runtime cases
The S0 factual cases are:
1. Launch one stock rover headless.
2. Observe PX4 health and ROS 2 telemetry.
3. Prove Gazebo `/clock` is authoritative.
4. Prove pause freezes simulation time.
5. Prove one single-step advances the declared amount.
6. Prove 1× and 2× speed behavior and measure real-time factor.
7. Stop through the intended lifecycle.
8. Prove no owned process remains.
9. Repeat from a clean start.
QGroundControl is diagnostic only and cannot become the Mission Core command
owner.
## Evidence manifest
Create a private/redacted evidence directory outside normal Git. Its manifest:
```yaml
schema_version: missioncore.simulation-s0-evidence/v1
profile_sha256: <sha256 of qualification-profile.yaml>
checks:
- id: worker-inventory
status: pass
artifact: worker-inventory.redacted.json
sha256: <artifact sha256>
note: Redacted host and resource inventory.
```
The manifest must contain exactly all IDs from `required_evidence`. Artifact
paths are relative, confined regular files. The doctor rejects missing,
additional, changed or cross-generation evidence.
Validate without writing:
```bash
uv run missioncore-sim s0 doctor \
--profile simulation/s0/qualification-profile.yaml \
--evidence /path/to/redacted/evidence.yaml \
--json
```
## Verdict rules
- `GO`: target host matches, every pin is accepted, D-only and disk thresholds
pass, required tools exist and all digest-bound evidence validates.
- `INCOMPLETE`: work remains or the doctor is running on a development host.
- `BLOCKED`: contract, storage, tools or evidence fail validation.
Publish the factual result to MISSIONCOR-40 with exact repository commit,
profile SHA-256, files touched and validation performed.

View File

@ -30,6 +30,7 @@ missioncore-plugin-sdk = { path = "packages/plugin-sdk", editable = true }
[project.scripts]
k1link = "k1link.device_plugins.xgrids_k1.cli:app"
missioncore-sim = "k1link.simulation.cli:app"
[dependency-groups]
dev = [

View File

@ -0,0 +1,183 @@
schema_version: missioncore.simulation-s0-profile/v1
profile_id: ai-worker-px4-gazebo-v1
target_host:
windows_release: windows-11
virtualization: wsl2
linux_distribution: ubuntu-24.04
architecture: x86_64
storage:
windows_root: 'D:\NDC_MISSIONCORE'
wsl_root: /mnt/d/NDC_MISSIONCORE
minimum_free_gib: 300
stop_below_gib: 200
forbid_windows_system_drive: true
mutable_paths:
- id: wsl
windows_path: 'D:\NDC_MISSIONCORE\wsl'
wsl_path: /mnt/d/NDC_MISSIONCORE/wsl
- id: docker
windows_path: 'D:\NDC_MISSIONCORE\docker'
wsl_path: /mnt/d/NDC_MISSIONCORE/docker
- id: source
windows_path: 'D:\NDC_MISSIONCORE\simulation\source'
wsl_path: /mnt/d/NDC_MISSIONCORE/simulation/source
- id: build
windows_path: 'D:\NDC_MISSIONCORE\simulation\build'
wsl_path: /mnt/d/NDC_MISSIONCORE/simulation/build
- id: cache
windows_path: 'D:\NDC_MISSIONCORE\simulation\cache'
wsl_path: /mnt/d/NDC_MISSIONCORE/simulation/cache
- id: runtime
windows_path: 'D:\NDC_MISSIONCORE\simulation\runtime'
wsl_path: /mnt/d/NDC_MISSIONCORE/simulation/runtime
- id: artifacts
windows_path: 'D:\NDC_MISSIONCORE\simulation\artifacts'
wsl_path: /mnt/d/NDC_MISSIONCORE/simulation/artifacts
- id: datasets
windows_path: 'D:\NDC_MISSIONCORE\simulation\datasets'
wsl_path: /mnt/d/NDC_MISSIONCORE/simulation/datasets
clock:
authority: gazebo:/clock
ros_use_sim_time: true
px4_uxrce_dds_sync_enabled: false
canonical_unit: nanoseconds
utc_role: provenance-only
authority:
simulation_or_shadow_only: true
actuator_authority: false
navigation_or_safety_accepted: false
direct_actuator_setpoints_allowed: false
allowed_setpoint_profiles:
- rover-speed-steering/v1
- rover-speed-yaw-rate/v1
ros:
domain_id: 42
namespace: /missioncore/polygon/s0
components:
- id: ros2
source_url: https://docs.ros.org/en/jazzy/
requested_ref: jazzy
resolved_version:
resolved_commit:
license: mixed-apache-2.0-bsd
qualification_state: pending-worker-resolution
- id: gazebo
source_url: https://gazebosim.org/docs/harmonic/
requested_ref: harmonic
resolved_version:
resolved_commit:
license: apache-2.0
qualification_state: pending-worker-resolution
- id: px4-autopilot
source_url: https://github.com/PX4/PX4-Autopilot
requested_ref: v1.17.0
resolved_version: v1.17.0
resolved_commit: d6f12ad1c4f70ad3230afd7d86e971421e02fef4
license: bsd-3-clause
qualification_state: candidate
- id: px4-msgs
source_url: https://github.com/PX4/px4_msgs
requested_ref: v1.17.0
resolved_version: v1.17.0
resolved_commit: 86d8239e962f6939e05c3737784f60c02fa884db
license: bsd-3-clause
qualification_state: candidate
- id: px4-gazebo-models
source_url: https://github.com/PX4/PX4-gazebo-models
requested_ref: main@2026-07-24
resolved_version:
resolved_commit: e00d3b9cde682dbcb3bf6f30a2f2b8ef4325dae8
license: bsd-3-clause
qualification_state: candidate
- id: micro-xrce-dds-agent
source_url: https://github.com/eProsima/Micro-XRCE-DDS-Agent
requested_ref: v2.4.3
resolved_version: v2.4.3
resolved_commit: 73622810d984349b80bbac0ef55fc0b694d62222
license: apache-2.0
qualification_state: candidate
- id: nav2
source_url: https://github.com/ros-navigation/navigation2
requested_ref: jazzy@2026-07-24
resolved_version:
resolved_commit: c92ea2fd9008f50c0ec8447610800214b2a0dafb
license: apache-2.0
qualification_state: candidate
ports:
- id: micro-xrce-dds
host_scope: worker-wsl
protocol: udp
bind: 127.0.0.1
port: 8888
owner: micro-xrce-dds-agent
- id: px4-mavlink-sitl
host_scope: worker-wsl
protocol: udp
bind: 127.0.0.1
port: 14540
owner: px4-sitl
- id: qgroundcontrol-reference
host_scope: worker-wsl
protocol: udp
bind: 127.0.0.1
port: 14550
owner: px4-sitl
- id: simulation-orchestrator-api
host_scope: worker-wsl
protocol: tcp
bind: 127.0.0.1
port: 18080
owner: simulation-orchestrator
- id: simulation-telemetry
host_scope: worker-wsl
protocol: tcp
bind: 127.0.0.1
port: 18081
owner: simulation-orchestrator
processes:
- id: simulation-orchestrator
parent:
shutdown_order: 60
- id: gazebo-server
parent: simulation-orchestrator
shutdown_order: 50
- id: px4-sitl
parent: simulation-orchestrator
shutdown_order: 40
- id: micro-xrce-dds-agent
parent: simulation-orchestrator
shutdown_order: 30
- id: ros2-bridge
parent: simulation-orchestrator
shutdown_order: 20
- id: nav2
parent: simulation-orchestrator
shutdown_order: 10
required_tools:
- git
- docker
- gz
- make
- colcon
- ros2
- MicroXRCEAgent
required_evidence:
- worker-inventory
- d-only-physical-placement
- px4-stock-rover-launch
- ros2-telemetry
- authoritative-simulation-clock
- pause-step-speed
- clean-stop-no-orphans
- resource-baseline-1x
- resource-baseline-2x

View File

@ -0,0 +1,21 @@
"""Mission Core qualification and simulation boundaries."""
from k1link.simulation.s0 import (
CheckStatus,
DoctorVerdict,
S0DoctorReport,
S0Profile,
S0ProfileError,
load_s0_profile,
run_s0_doctor,
)
__all__ = [
"CheckStatus",
"DoctorVerdict",
"S0DoctorReport",
"S0Profile",
"S0ProfileError",
"load_s0_profile",
"run_s0_doctor",
]

View File

@ -0,0 +1,98 @@
from __future__ import annotations
import json
from pathlib import Path
from typing import Annotated
import typer
from rich.console import Console
from rich.table import Table
from k1link.simulation import S0ProfileError, run_s0_doctor
app = typer.Typer(
name="missioncore-sim",
help="Fail-closed qualification tooling for the Mission Core Polygon.",
no_args_is_help=True,
)
s0_app = typer.Typer(
help="SIM S0 compatibility and infrastructure qualification.",
no_args_is_help=True,
)
app.add_typer(s0_app, name="s0")
console = Console()
def _default_profile() -> Path:
return Path(__file__).resolve().parents[3] / "simulation" / "s0" / "qualification-profile.yaml"
DEFAULT_PROFILE = _default_profile()
@s0_app.command("doctor")
def s0_doctor(
profile: Annotated[
Path,
typer.Option(
"--profile",
help="Strict SIM S0 qualification profile.",
exists=True,
dir_okay=False,
readable=True,
),
] = DEFAULT_PROFILE,
evidence: Annotated[
Path | None,
typer.Option(
"--evidence",
help="Optional digest-bound target-worker evidence manifest.",
exists=True,
dir_okay=False,
readable=True,
),
] = None,
json_output: Annotated[
bool,
typer.Option("--json", help="Emit the complete report as JSON."),
] = False,
) -> None:
"""Inspect contracts and local target readiness without changing the host."""
try:
report = run_s0_doctor(profile, evidence_path=evidence)
except (OSError, S0ProfileError) as exc:
if json_output:
console.print_json(
json.dumps(
{
"schema_version": "missioncore.simulation-s0-doctor/v1",
"verdict": "blocked",
"error": str(exc),
}
)
)
else:
console.print(f"[red]SIM S0 profile rejected:[/red] {exc}")
raise typer.Exit(code=2) from exc
payload = report.to_dict()
if json_output:
console.print_json(json.dumps(payload))
return
table = Table(title=f"SIM S0 doctor — {report.profile_id}")
table.add_column("Check")
table.add_column("Status")
table.add_column("Detail")
colors = {"pass": "green", "fail": "red", "unknown": "yellow"}
for check in report.checks:
status = check.status.value
styled_status = f"[{colors[status]}]{status}[/{colors[status]}]"
table.add_row(check.identifier, styled_status, check.detail)
console.print(table)
console.print(f"Verdict: [bold]{report.verdict.value.upper()}[/bold]")
if __name__ == "__main__":
app()

1006
src/k1link/simulation/s0.py Normal file

File diff suppressed because it is too large Load Diff

227
tests/test_simulation_s0.py Normal file
View File

@ -0,0 +1,227 @@
from __future__ import annotations
import hashlib
import json
from pathlib import Path
import pytest
import yaml
from typer.testing import CliRunner
from k1link.simulation import (
CheckStatus,
DoctorVerdict,
S0ProfileError,
load_s0_profile,
run_s0_doctor,
)
from k1link.simulation.cli import app
PROFILE = Path(__file__).resolve().parents[1] / "simulation" / "s0" / "qualification-profile.yaml"
runner = CliRunner()
def _profile_document() -> dict[str, object]:
loaded = yaml.safe_load(PROFILE.read_text(encoding="utf-8"))
assert isinstance(loaded, dict)
return loaded
def _write_profile(tmp_path: Path, document: dict[str, object]) -> Path:
path = tmp_path / "profile.yaml"
path.write_text(yaml.safe_dump(document, sort_keys=False), encoding="utf-8")
return path
def test_canonical_s0_profile_is_strict_and_safe() -> None:
profile = load_s0_profile(PROFILE)
assert profile.profile_id == "ai-worker-px4-gazebo-v1"
assert profile.storage.windows_root.drive.upper() == "D:"
assert profile.storage.wsl_root.as_posix() == "/mnt/d/NDC_MISSIONCORE"
assert profile.clock.authority == "gazebo:/clock"
assert profile.clock.ros_use_sim_time is True
assert profile.clock.px4_uxrce_dds_sync_enabled is False
assert profile.authority.simulation_or_shadow_only is True
assert profile.authority.actuator_authority is False
assert profile.authority.navigation_or_safety_accepted is False
assert profile.authority.direct_actuator_setpoints_allowed is False
assert profile.ros.domain_id == 42
assert {item.identifier for item in profile.components} >= {
"px4-autopilot",
"px4-msgs",
"gazebo",
"nav2",
}
assert {item.identifier for item in profile.processes} == {
"simulation-orchestrator",
"gazebo-server",
"px4-sitl",
"micro-xrce-dds-agent",
"ros2-bridge",
"nav2",
}
def test_profile_rejects_real_actuator_authority(tmp_path: Path) -> None:
document = _profile_document()
authority = document["authority"]
assert isinstance(authority, dict)
authority["actuator_authority"] = True
with pytest.raises(S0ProfileError, match="cannot grant real"):
load_s0_profile(_write_profile(tmp_path, document))
def test_profile_rejects_mutable_c_drive_path(tmp_path: Path) -> None:
document = _profile_document()
storage = document["storage"]
assert isinstance(storage, dict)
mutable = storage["mutable_paths"]
assert isinstance(mutable, list)
first = mutable[0]
assert isinstance(first, dict)
first["windows_path"] = r"C:\NDC_MISSIONCORE\wsl"
with pytest.raises(S0ProfileError, match="escapes D-only root"):
load_s0_profile(_write_profile(tmp_path, document))
def test_profile_rejects_duplicate_scoped_port(tmp_path: Path) -> None:
document = _profile_document()
ports = document["ports"]
assert isinstance(ports, list)
duplicate = dict(ports[0])
duplicate["id"] = "duplicate-port"
duplicate["owner"] = "simulation-orchestrator"
ports.append(duplicate)
with pytest.raises(S0ProfileError, match="duplicates a scoped binding"):
load_s0_profile(_write_profile(tmp_path, document))
def test_doctor_never_claims_go_without_target_evidence(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
"k1link.simulation.s0._target_host_match",
lambda target: (False, "test host is not target"),
)
report = run_s0_doctor(PROFILE)
assert report.verdict is DoctorVerdict.INCOMPLETE
assert report.target_host_match is False
assert any(
check.identifier == "factual-evidence" and check.status is CheckStatus.UNKNOWN
for check in report.checks
)
payload = report.to_dict()
assert payload["authority"] == {
"simulation_or_shadow_only": True,
"actuator_authority": False,
"navigation_or_safety_accepted": False,
"direct_actuator_setpoints_allowed": False,
}
def test_doctor_validates_digest_bound_evidence(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
monkeypatch.setattr(
"k1link.simulation.s0._target_host_match",
lambda target: (False, "test host is not target"),
)
profile = load_s0_profile(PROFILE)
checks: list[dict[str, object]] = []
for identifier in profile.required_evidence:
artifact = tmp_path / f"{identifier}.txt"
artifact.write_text(f"evidence for {identifier}\n", encoding="utf-8")
checks.append(
{
"id": identifier,
"status": "pass",
"artifact": artifact.name,
"sha256": hashlib.sha256(artifact.read_bytes()).hexdigest(),
"note": "synthetic unit-test evidence",
}
)
evidence = tmp_path / "evidence.yaml"
evidence.write_text(
yaml.safe_dump(
{
"schema_version": "missioncore.simulation-s0-evidence/v1",
"profile_sha256": hashlib.sha256(PROFILE.read_bytes()).hexdigest(),
"checks": checks,
},
sort_keys=False,
),
encoding="utf-8",
)
report = run_s0_doctor(PROFILE, evidence_path=evidence)
assert report.verdict is DoctorVerdict.INCOMPLETE
assert any(
check.identifier == "factual-evidence" and check.status is CheckStatus.PASS
for check in report.checks
)
def test_doctor_rejects_changed_evidence_artifact(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
monkeypatch.setattr(
"k1link.simulation.s0._target_host_match",
lambda target: (False, "test host is not target"),
)
profile = load_s0_profile(PROFILE)
checks: list[dict[str, object]] = []
artifacts: list[Path] = []
for identifier in profile.required_evidence:
artifact = tmp_path / f"{identifier}.txt"
artifact.write_text("original\n", encoding="utf-8")
artifacts.append(artifact)
checks.append(
{
"id": identifier,
"status": "pass",
"artifact": artifact.name,
"sha256": hashlib.sha256(artifact.read_bytes()).hexdigest(),
"note": "synthetic unit-test evidence",
}
)
evidence = tmp_path / "evidence.yaml"
evidence.write_text(
yaml.safe_dump(
{
"schema_version": "missioncore.simulation-s0-evidence/v1",
"profile_sha256": hashlib.sha256(PROFILE.read_bytes()).hexdigest(),
"checks": checks,
},
sort_keys=False,
),
encoding="utf-8",
)
artifacts[0].write_text("changed\n", encoding="utf-8")
report = run_s0_doctor(PROFILE, evidence_path=evidence)
assert report.verdict is DoctorVerdict.BLOCKED
assert any(
check.identifier == "factual-evidence"
and check.status is CheckStatus.FAIL
and "digest changed" in check.detail
for check in report.checks
)
def test_simulation_doctor_cli_emits_json_without_side_effects() -> None:
result = runner.invoke(app, ["s0", "doctor", "--profile", str(PROFILE), "--json"])
assert result.exit_code == 0
payload = json.loads(result.stdout)
assert payload["schema_version"] == "missioncore.simulation-s0-doctor/v1"
assert payload["profile_id"] == "ai-worker-px4-gazebo-v1"
assert payload["verdict"] in {"incomplete", "blocked"}
assert payload["authority"]["actuator_authority"] is False