ReviewOS

also looking at this

stacks/drone

feat: add VTOL scenario playback to the mission workspace

#84
Merged chris wants to merge codex/owned-vtol-workspace into main
26 files +1336 -66

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

.github/workflows/ci.ymlmodified+4-0
Changes to .github/workflows/ci.yml
@@ -150,6 +150,10 @@ jobs:
150150 working-directory: ${{ runner.temp }}
151151 - run: $GITHUB_WORKSPACE/storage/runtime-build/cli/drone vtol:hover --scenario host-loss --electrical
152152 working-directory: ${{ runner.temp }}
153 - run: $GITHUB_WORKSPACE/storage/runtime-build/cli/drone vtol:simulate
154 working-directory: ${{ runner.temp }}
155 - run: $GITHUB_WORKSPACE/storage/runtime-build/cli/drone vtol:simulate --electrical
156 working-directory: ${{ runner.temp }}
153157 - run: $GITHUB_WORKSPACE/storage/runtime-build/cli/drone authority:simulate --scenario pi-loss --seconds 4
154158 working-directory: ${{ runner.temp }}
155159 - run: $GITHUB_WORKSPACE/storage/runtime-build/cli/drone drivers:bench --driver ardupilot --drop-first-ack
README.mdmodified+3-2
Changes to README.md
@@ -78,8 +78,9 @@ outputs remain inhibited. Read the [shared contract](docs/vehicles.md), [air mod
7878
7979`./buddy vtol:hover` separately exercises [VTOL development](docs/vehicles-vtol.md):
8080vertical takeoff, a nearby hover waypoint and contact-gated landing. The TypeScript library also
81includes a bounded modeled front/cruise/back experiment with stable-phase pause and resume. The
82command stays hover-only. Add `--electrical` for fictional propulsion/battery load and delayed energy measurements.
81includes a bounded modeled front/cruise/back experiment with stable-phase pause and resume.
82`./buddy vtol:simulate` runs its nominal route through vertical landing and reports the qualified
83mode observations. Add `--electrical` to either command for fictional propulsion/battery load and delayed energy measurements.
8384Plans can explicitly declare bounded hover-and-land transition recovery. Complete recovery/abort envelopes,
8485measured hybrid energy qualification and a selectable VTOL mission workspace remain open.
8586
app/Commands/VtolSimulate.tsadded+28-0
Changes to app/Commands/VtolSimulate.ts
@@ -0,0 +1,28 @@
1import { defineCommand } from '@stacksjs/cli'
2import { print, printError } from '@stacksjs/cli/output'
3import { createVtolTransitionProfile } from '../../resources/functions/drone/vehicles/vtol/transition-profile'
4import { runVehicleSimulation } from '../../resources/functions/drone/vehicles/runtime'
5
6export default defineCommand({
7 name: 'vtol:simulate',
8 description: 'Exercise the fictional owned front/cruise/back/landing route; no physical control',
9 options: {
10 '--seconds <n>': { description: 'Maximum virtual duration, 2..180 seconds', default: '160' },
11 '--electrical': { description: 'Use fictional electrical load and delayed battery measurements', default: false },
12 },
13 handle(options) {
14 try {
15 const result = runVehicleSimulation(createVtolTransitionProfile({ electrical: options.electrical }), { seconds: Number(options.seconds) })
16 const { trace, path, ...report } = result
17 const transitionEvidence = trace.rows.flatMap(row => row.observation.transition?.outcome === 'confirmed' ? [row.observation.transition] : [])
18 const completeTransitions = transitionEvidence.length === 3 && transitionEvidence.every((row, index) => row.taskId === ['initial-hover', 'front', 'back'][index] && row.phase === ['hover', 'cruise', 'hover'][index])
19 const observedFlight = path.some(row => row.positionM && row.positionM[2] < -1 && row.actuators.slice(0, 4).some(n => n > 0))
20 const landingContact = path.at(-1)?.measurement?.values.onSurface === true
21 const boardNeutral = result.board.actuators.every(n => n === 0) && result.board.expiresAtMs === null
22 const demonstrationMatched = result.status === 'completed' && result.fault === null && result.mission.taskId === 'land' && result.mission.reason === 'landed' && completeTransitions && observedFlight && landingContact && boardNeutral
23 print(JSON.stringify({ ...report, scope: 'development-lift-and-cruise', transitionEvidence, completeTransitions, observedFlight, landingContact, boardNeutral, demonstrationMatched, recordedMissionRows: trace.rows.length, evaluatedPathRows: path.length }, (_key, value) => typeof value === 'bigint' ? String(value) : value, 2))
24 if (!demonstrationMatched) process.exitCode = 1
25 }
26 catch (error) { printError(error instanceof Error ? error.message : String(error)); process.exitCode = 1 }
27 },
28})
bin/drone.tsmodified+2-0
Changes to bin/drone.ts
@@ -51,6 +51,7 @@ import PatrolReplay from '../app/Commands/PatrolReplay'
5151import AntennaBench from '../app/Commands/AntennaBench'
5252import AirSimulate from '../app/Commands/AirSimulate'
5353import VtolHover from '../app/Commands/VtolHover'
54import VtolSimulate from '../app/Commands/VtolSimulate'
5455import GroundSimulate from '../app/Commands/GroundSimulate'
5556import SurfaceSimulate from '../app/Commands/SurfaceSimulate'
5657import SubseaSimulate from '../app/Commands/SubseaSimulate'
@@ -128,6 +129,7 @@ PatrolReplay(app)
128129AntennaBench(app)
129130AirSimulate(app)
130131VtolHover(app)
132VtolSimulate(app)
131133GroundSimulate(app)
132134SurfaceSimulate(app)
133135SubseaSimulate(app)
docs/index.mdmodified+1-1
Changes to docs/index.md
@@ -56,7 +56,7 @@ surface and subsea profiles. [Optional external drivers](drivers.md) have
5656[acknowledged protocol services](driver-protocols.md); scripted peers are distinct
5757from [firmware SITL](sitl.md) and physical testing.
5858
59The separate [VTOL development guide](vehicles-vtol.md) covers the hover-only command and an
59The separate [VTOL development guide](vehicles-vtol.md) covers the hover-only and nominal full-route commands, and an
6060experimental TypeScript library profile for modeled front transition, cruise, back transition and
6161vertical landing, with stable-phase pause/resume and explicitly declared hover-and-land recovery
6262routes. Complete recovery/abort envelopes, hybrid energy qualification and the common VTOL mission
docs/roadmap/feature-coverage.mdmodified+1-1
Changes to docs/roadmap/feature-coverage.md
@@ -16,7 +16,7 @@ means there is no native implementation yet. A protocol-compatible transport is
1616| Diagnostics, logs, replay, firmware update and configuration provenance | Owned runtime journal, loss accounting, replay, CLI/viewer and [protected signed preparation, owned candidates, host slots, observed image/session health and fresh-policy rollback](../runtime-updates.md); full mission correlation, key repair, complete storage faults and board install/boot/rollback qualification remain pending | Named parameter read/write with bounded readback in the SITL adapter; no log/FTP services, full-table synchronization or reboot persistence qualification | [#9](https://github.com/stacksjs/drone/issues/9) |
1717| Missions, tasks, waypoints, terrain, rally and resume | Owned versioned graph, flat-terrain/fence/rally/energy validation, physics execution, pause/resume, recorded recovery, CLI and stx viewer; correlated durable checkpoints, driver translation and hardware evidence pending | Bounded SITL waypoint upload/download and acknowledged item selection/resume; no arming, automatic flight-mode selection or owned-plan translation | [#10](https://github.com/stacksjs/drone/issues/10) |
1818| Avoidance, route planning, precision landing, follow and orbit | Owned modeled range detection, inflated route planning, terrain-clearance/RTL/selected-site landing physics; metric follow/orbit goals; physical calibration and dynamic obstacle campaigns pending | No avoidance/landing integration | [#11](https://github.com/stacksjs/drone/issues/11) |
19| Fixed-wing, VTOL, heli, blimp and experimental airframes | Owned fixed-wing runway takeoff/survey/landing, hover-only VTOL command and [experimental VTOL library](../vehicles-vtol.md) with modeled front/cruise/back/vertical landing, pause/resume, opt-in hover-and-land recovery and fictional electrical load/battery/sensor accounting; complete recovery/abort envelopes, measured hybrid energy, native transition CLI/workspace, helicopter, soaring, blimp and other airframe families remain open | Current drivers reject non-quadcopter heartbeats | [#12](https://github.com/stacksjs/drone/issues/12) |
19| Fixed-wing, VTOL, heli, blimp and experimental airframes | Owned fixed-wing runway takeoff/survey/landing, hover-only and nominal full-route VTOL commands and [experimental VTOL library](../vehicles-vtol.md) with modeled front/cruise/back/vertical landing, pause/resume, opt-in hover-and-land recovery and fictional electrical load/battery/sensor accounting; complete recovery/abort envelopes, measured hybrid energy, CLI fault/operator scenarios, workspace selection, helicopter, soaring, blimp and other airframe families remain open | Current drivers reject non-quadcopter heartbeats | [#12](https://github.com/stacksjs/drone/issues/12) |
2020| Rovers, mowing, logistics and materials handling | Owned differential traction/braking, encoder/GNSS odometry, guarded mowing coverage and scoped material-drop simulation; hardware and broader chassis/implements pending | Not exposed | [#13](https://github.com/stacksjs/drone/issues/13) |
2121| Surface vessels, bathymetry, water sampling and harbours | Owned planar hull/current/wind, collision inhibition, corrected single-beam export, scoped sample ACK and energy-return simulation; physical survey/sampling and broader hulls pending | Not exposed | [#14](https://github.com/stacksjs/drone/issues/14) |
2222| Subsea ROV, depth hold, tether and leak handling | Owned eight-thruster 6DOF physics, pressure/DVL local estimation, depth hold, tether/leak supervision and scoped synthetic camera/sonar evidence; hardware, full hydrodynamics and physical inspection pending | Not exposed | [#15](https://github.com/stacksjs/drone/issues/15) |
docs/vehicles-vtol.mdmodified+27-7
Changes to docs/vehicles-vtol.md
@@ -74,8 +74,27 @@ new CLI scenario. Existing hover-only recordings keep their prior shapes.
7474is a separate development experiment on the same owned runtime. Its nominal mission observes initial
7575hover, takes off vertically to 15 m, requests a front transition, flies a cruise leg, requests back
7676transition, returns to the back request's capture point, observes settled hover, and lands vertically.
77A turning leg also has an actual completed model campaign. This profile is not registered as a CLI
78scenario or selectable workspace profile.
77A turning leg also has an actual completed model campaign. The native `vtol:simulate` command runs
78the nominal route. This profile is not a selectable workspace profile.
79
80```sh
81./buddy vtol:simulate
82./buddy vtol:simulate --electrical
83./storage/runtime-build/cli/drone vtol:simulate --electrical
84```
85
86The command defaults to 160 virtual seconds and accepts an integer `--seconds` from 2 to 180.
87It reports the existing runtime's actual status and the three qualified initial-hover, cruise and
88back-hover observations, including their event, task, reference and capture epochs. Exit 0 requires
89those ordered confirmations, observed estimated flight, completed landing with qualified modeled
90contact, no runtime fault, neutral board outputs and no active output lease. An incomplete route
91retains its partial observations and status and exits 1. The condensed report omits the full path
92and trace. The library still exposes both for replay and evaluation.
93
94Buddy discovers the typed command natively; the standalone binary registers the same definition.
95This command currently exposes the nominal route only. Fault scenarios, operator schedules and
96recovery route selection remain library capabilities. Simulation completion does not qualify
97physical transitions, arbitrary flight envelopes or hardware landing.
7998
8099The controller receives measured relative airspeed, differential pressure, airflow angles and modeled
81100rotor/pusher response, together with the qualified raw gyro and estimated navigation. Delayed airflow
@@ -150,7 +169,8 @@ and releases outputs. Neither result establishes an airborne landing.
150169Task execution time stops during pause. Global endurance and the declared energy charge continue,
151170so a later scheduled resume can be preempted by reserve, endurance, health or authority refusal.
152171Operator actions never extend the original planning allowance or rearm revoked authority. The
153transition library and these actions remain separate from the hover-only CLI and common workspace.
172operator schedules remain library-only; neither CLI command exposes them, and the common workspace
173does not select this VTOL profile.
154174
155175## Declared transition recovery routes
156176
@@ -209,11 +229,11 @@ campaigns do not qualify arbitrary wind, failed lift rotors, every transition st
209229
210230## Fictional electrical model
211231
212Select `--electrical` in the hover command, or pass `{ electrical: true }` to
232Select `--electrical` in either VTOL command, or pass `{ electrical: true }` to
213233`createVtolHoverProfile('hover', options)` or `createVtolTransitionProfile(options)` in TypeScript.
214234The default/disabled profiles keep their prior reports and declared-ceiling energy accounting.
215The same hover option runs through Buddy and the copied standalone CLI. Full transition execution
216remains a library feature.
235Both commands run through Buddy and the copied standalone CLI. The nominal full-route command
236uses the same experimental transition library profile.
217237
218238Each session owns its electrical state. The plant uses actual lagged individual rotor and pusher
219239thrust, with these declared coefficients:
@@ -272,7 +292,7 @@ it does not model a seized motor's electrical current.
272292
273293[Issue #12](https://github.com/stacksjs/drone/issues/12) remains open. The experimental nominal
274294controller and opt-in recovery routes do not establish complete VTOL support. Complete recovery/abort
275envelopes, fresh-process restoration, identified hybrid power/voltage/cell behavior, native transition CLI and
295envelopes, fresh-process restoration, identified hybrid power/voltage/cell behavior, CLI fault/operator scenarios and
276296the common mission workspace still require implementation and qualification. Wind campaigns and
277297broader mission/envelope coverage remain necessary. Full TECS/soaring, tiltrotor/tailsitter mechanisms,
278298helicopters and other airframe families remain separate work. Native board control, measured airframe
docs/vehicles.mdmodified+4-3
Changes to docs/vehicles.md
@@ -61,11 +61,12 @@ recompute mission replay, reject altered traces and exercise payload uncertainty
6161All evidence is owned physics simulation. The Pico neutral-only board image does not run these
6262TypeScript controllers. Native board execution, external firmware SITL, HIL and measured physical
6363vehicle acceptance remain separate gates. A separate [VTOL development guide](vehicles-vtol.md)
64covers the hover-only command and an experimental lift-and-cruise library profile. The latter
64covers the hover-only command and an experimental lift-and-cruise library profile with a native
65`vtol:simulate` nominal-route command. The latter
6566executes a bounded modeled front/cruise/back/vertical-landing route with stable-phase pause and
6667resume, plus explicitly declared hover-and-land transition recovery. Complete recovery/abort
67envelopes, hybrid energy qualification,
68a native transition command and a VTOL selection in the common workspace remain open. Helicopter,
68envelopes, measured hybrid energy qualification,
69CLI fault/operator scenarios and a VTOL selection in the common workspace remain open. Helicopter,
6970soaring, blimp and other airframe families remain explicit roadmap work.
7071
7172Read [air](vehicles-air.md), [ground](vehicles-ground.md), [surface](vehicles-surface.md), [subsea](vehicles-subsea.md),
tests/tsconfig.runtime.jsonmodified+1-0
Changes to tests/tsconfig.runtime.json
@@ -77,6 +77,7 @@
7777 "./unit/vtol-runtime.test.ts",
7878 "./unit/vtol-energy.test.ts",
7979 "./unit/vtol-cli.test.ts",
80 "./unit/vtol-transition-cli.test.ts",
8081 "./unit/vtol-mode-mission.test.ts",
8182 "./unit/vtol-transition.test.ts",
8283 "./unit/ground.test.ts",
tests/unit/vtol-transition-cli.test.tsadded+73-0
Changes to tests/unit/vtol-transition-cli.test.ts
@@ -0,0 +1,73 @@
1import { expect, test } from 'bun:test'
2import { command } from '@stacksjs/testing'
3
4test('CI qualifies both full-route commands outside the checkout', async () => {
5 const workflow = Bun.YAML.parse(await Bun.file(new URL('../../.github/workflows/ci.yml', import.meta.url)).text()) as {
6 jobs: { check: { steps: { run?: string, 'working-directory'?: string }[] } }
7 }
8 expect(workflow.jobs.check.steps.filter(step => step.run?.startsWith('$GITHUB_WORKSPACE/storage/runtime-build/cli/drone vtol:simulate')).map(step => ({ command: step.run, directory: step['working-directory'] }))).toEqual([
9 { command: '$GITHUB_WORKSPACE/storage/runtime-build/cli/drone vtol:simulate', directory: '$' },
10 { command: '$GITHUB_WORKSPACE/storage/runtime-build/cli/drone vtol:simulate --electrical', directory: '$' },
11 ])
12})
13
14test('native VTOL full-route command reports qualified initial hover, cruise, back hover and landing', async () => {
15 const result = await command(['vtol:simulate']).withTimeout(60000)
16 expect(result.exitCode).toBe(0)
17 const report = JSON.parse(result.stdout)
18 expect(report.status).toBe('completed')
19 expect(report.scope).toBe('development-lift-and-cruise')
20 expect(report.demonstrationMatched).toBe(true)
21 expect(report.transitionEvidence.map((row: { taskId: string, phase: string }) => [row.taskId, row.phase])).toEqual([
22 ['initial-hover', 'hover'], ['front', 'cruise'], ['back', 'hover'],
23 ])
24 for (const row of report.transitionEvidence) {
25 expect(row.eventId).toBeString()
26 expect(row.reference).toBeString()
27 expect(row.outcome).toBe('confirmed')
28 expect(row.measurementCapturedAtMs).toBeLessThanOrEqual(row.capturedAtMs)
29 }
30 expect(report.observedFlight).toBe(true)
31 expect(report.landingContact).toBe(true)
32 expect(report.mission.state).toBe('completed')
33 expect(report.mission.taskId).toBe('land')
34 expect(report.mission.reason).toBe('landed')
35 expect(report.board.actuators).toEqual([0, 0, 0, 0, 0, 0, 0, 0])
36 expect(report.board.expiresAtMs).toBeNull()
37 expect(report.physicalFlightEnabled).toBe(false)
38 expect(report.boardOutputsInhibited).toBe(true)
39 expect(report).not.toHaveProperty('trace')
40 expect(report).not.toHaveProperty('path')
41 expect(report).not.toHaveProperty('energy')
42}, 90000)
43
44test('native full-route electrical option retains qualified landing and modeled energy evidence', async () => {
45 const result = await command(['vtol:simulate', '--electrical']).withTimeout(60000)
46 expect(result.exitCode).toBe(0)
47 const report = JSON.parse(result.stdout)
48 expect(report.demonstrationMatched).toBe(true)
49 expect(report.completeTransitions).toBe(true)
50 expect(report.landingContact).toBe(true)
51 expect(report.energy.model).toEqual({ evidence: 'modeled-vehicle-battery-v1', capacityWh: 100, maximumSourceW: 1061 })
52 expect(report.energy.lastObservation.energyUsedWh).toBeGreaterThan(0)
53 expect(report.energy.lastObservation.capturedAtMs).toBeLessThan(report.energy.lastObservation.observedAtMs)
54 expect(report.energy.lastObservation.cutoff).toBe(false)
55}, 90000)
56
57test('native full-route command preserves partial outcomes and rejects invalid duration', async () => {
58 const short = await command(['vtol:simulate', '--seconds', '2']).withTimeout(60000)
59 expect(short.exitCode).toBe(1)
60 const report = JSON.parse(short.stdout)
61 expect(report.status).toBe('duration-exhausted')
62 expect(report.demonstrationMatched).toBe(false)
63 expect(report.completeTransitions).toBe(false)
64 expect(report.landingContact).toBe(false)
65 expect(report.mission.state).toBe('running')
66 expect(report.boardNeutral).toBe(true)
67 expect(report.transitionEvidence.map((row: { taskId: string }) => row.taskId)).toEqual(['initial-hover'])
68 for (const seconds of ['NaN', '1', '181', '2.5']) {
69 const invalid = await command(['vtol:simulate', '--seconds', seconds]).withTimeout(60000)
70 expect(invalid.exitCode).toBe(1)
71 expect(invalid.stdout).toBe('')
72 }
73}, 90000)