Docking approach — Progress / Mir 1997: abort means ABORT
On 25 June 1997 the uncrewed Progress M-34 freighter, under manual TORU control, came in on Mir too fast and off-axis. The crew saw it late, the braking command came late, and Progress struck the Spektr module — puncturing the station and depressurizing a wing of it. The crew survived by sealing the hatch. The lesson every rendezvous system learned: a bad approach is not "salvaged", it is killed, and the vehicle retreats. You re-plan from scratch. Here the approach is a state machine where ABORTED IS TERMINAL: once an abort fires, there is no edge back to any approach state — a new rendezvous is a new machine instance, with new ground review. The final-approach gate is also a memory invariant: a closing rate above the structural capture limit cannot even be recorded as commanded.
A Soma program from the aerospace part of the verified corpus: 116 lines, cells DockingApproach, DockingTests. It passes soma check with soma 3.2.4.
How it is built
The cell DockingApproach declares 6 signals in its face: begin_approach(id: String) -> Map, go_final(id: String, closing_cm_s: Int) -> Map, soft_capture(id: String) -> Map, hard_dock(id: String) -> Map, abort_approach(id: String) -> Map and approach_phase(id: String) -> String; keeps 1 memory slot: closing_cm_s (Map<String, Int>, persistent and consistent) under the invariant closing_cm_s >= 0 && closing_cm_s <= 30. Its state machine approach starts in station_keeping and allows 6 transitions: station_keeping -> corridor, corridor -> final_approach, final_approach -> captured, captured -> hard_dock_complete, corridor -> aborted and final_approach -> aborted. aborted and hard_dock_complete are terminal. Handlers: begin_approach (line 49), go_final (line 55), soft_capture (line 67), hard_dock (line 73), abort_approach (line 79) and approach_phase (line 85).
The file also holds 1 test cell (DockingTests) with 16 assertions, run by soma test.
The source
docking_approach.cell
// ============================================================================
// DOCKING APPROACH — Progress / Mir 1997: abort means ABORT
// ============================================================================
//
// On 25 June 1997 the uncrewed Progress M-34 freighter, under manual
// TORU control, came in on Mir too fast and off-axis. The crew saw it
// late, the braking command came late, and Progress struck the Spektr
// module — puncturing the station and depressurizing a wing of it. The
// crew survived by sealing the hatch. The lesson every rendezvous system
// learned: a bad approach is not "salvaged", it is killed, and the
// vehicle retreats. You re-plan from scratch.
//
// Here the approach is a state machine where ABORTED IS TERMINAL: once
// an abort fires, there is no edge back to any approach state — a new
// rendezvous is a new machine instance, with new ground review. The
// final-approach gate is also a memory invariant: a closing rate above
// the structural capture limit cannot even be recorded as commanded.
// ============================================================================
cell DockingApproach {
face {
signal begin_approach(id: String) -> Map
signal go_final(id: String, closing_cm_s: Int) -> Map
signal soft_capture(id: String) -> Map
signal hard_dock(id: String) -> Map
signal abort_approach(id: String) -> Map
signal approach_phase(id: String) -> String
}
memory {
// commanded closing rate, cm/s — the docking mechanism is rated
// to absorb 30; a faster command is refused at the write
closing_cm_s: Map<String, Int> [persistent, consistent]
invariant closing_cm_s >= 0 && closing_cm_s <= 30
}
// ABSENT: aborted -> anything (abort is terminal)
// ABSENT: station_keeping -> soft_capture (no skipping the corridor)
state approach {
initial: station_keeping
station_keeping -> corridor // enter the approach corridor
corridor -> final_approach
final_approach -> captured // soft capture: latches engaged
captured -> hard_dock_complete // terminal: mated, hooks closed
corridor -> aborted // terminal
final_approach -> aborted // terminal
}
on begin_approach(id: String) {
let r = try { transition(id, "corridor") }
if r.error != () { return map("ok", false, "reason", "{r.error}") }
return map("ok", true, "phase", get_status(id))
}
on go_final(id: String, closing_cm_s_v: Int) {
// the rate wall: even a confused operator cannot command a
// closing rate the mechanism cannot absorb
let w = try { closing_cm_s.set(id, closing_cm_s_v) }
if w.error != () {
return map("ok", false, "reason", "closing rate {closing_cm_s_v} cm/s exceeds capture rating")
}
let r = try { transition(id, "final_approach") }
if r.error != () { return map("ok", false, "reason", "{r.error}") }
return map("ok", true, "phase", get_status(id), "rate", closing_cm_s_v)
}
on soft_capture(id: String) {
let r = try { transition(id, "captured") }
if r.error != () { return map("ok", false, "reason", "{r.error}") }
return map("ok", true, "phase", get_status(id))
}
on hard_dock(id: String) {
let r = try { transition(id, "hard_dock_complete") }
if r.error != () { return map("ok", false, "reason", "{r.error}") }
return map("ok", true, "phase", get_status(id))
}
on abort_approach(id: String) {
let r = try { transition(id, "aborted") }
if r.error != () { return map("ok", false, "reason", "{r.error}") }
return map("ok", true, "phase", "aborted")
}
on approach_phase(id: String) { return get_status(id) }
}
cell test DockingTests {
rules {
// no skipping the corridor: capture from station keeping is absent
assert_fails transition("p1", "captured")
// a Progress-M-34-speed approach cannot even be COMMANDED:
// the closing-rate write refuses anything past the rating
assert begin_approach("p1").ok == true
assert go_final("p1", 120).ok == false
assert_fails closing_cm_s.set("p1", 120)
assert approach_phase("p1") == "corridor"
// a sane rate proceeds to capture and hard dock
assert go_final("p1", 8).ok == true
assert soft_capture("p1").ok == true
assert hard_dock("p1").ok == true
assert_fails transition("p1", "corridor") // mated is terminal
// ABORT IS TERMINAL: a killed approach never creeps back in
assert begin_approach("p2").ok == true
assert go_final("p2", 12).ok == true
assert abort_approach("p2").ok == true
assert_fails transition("p2", "final_approach")
assert_fails transition("p2", "corridor")
assert_fails transition("p2", "captured")
assert soft_capture("p2").ok == false
}
}What the toolchain says
$ soma check docking_approach.cell
warning: parameter 2 of `go_final`: the face calls it `closing_cm_s`, the handler `closing_cm_s_v` — an HTTP JSON body is matched by NAME (the handler's), and `soma describe` publishes the face's: use one name
--> docking_approach.cell:23:9
|
23 | signal go_final(id: String, closing_cm_s: Int) -> Map
| ^
✓ 1 warning(s), no errors.$ soma test docking_approach.cell
test DockingTests ...
✓ assert_fails transition("p1", "captured") — raised invalid transition: station_keeping → captured. Current state: 'station_keeping'. Valid targets: [corridor]
✓ assert begin_approach("p1").ok == true
✓ assert go_final("p1", 120).ok == false
✓ assert_fails closing_cm_s.set("p1", 120) — raised memory invariant violated on 'closing_cm_s': closing_cm_s >= 0 && closing_cm_s <= 30 — rejected write of 120 (key "p1"); the slot is unchanged
✓ assert approach_phase("p1") == "corridor"
✓ assert go_final("p1", 8).ok == true
✓ assert soft_capture("p1").ok == true
✓ assert hard_dock("p1").ok == true
✓ assert_fails transition("p1", "corridor") — raised invalid transition: hard_dock_complete → corridor. Current state: 'hard_dock_complete'. Valid targets: []
✓ assert begin_approach("p2").ok == true
✓ assert go_final("p2", 12).ok == true
✓ assert abort_approach("p2").ok == true
✓ assert_fails transition("p2", "final_approach") — raised invalid transition: aborted → final_approach. Current state: 'aborted'. Valid targets: []
✓ assert_fails transition("p2", "corridor") — raised invalid transition: aborted → corridor. Current state: 'aborted'. Valid targets: []
✓ assert_fails transition("p2", "captured") — raised invalid transition: aborted → captured. Current state: 'aborted'. Valid targets: []
✓ assert soft_capture("p2").ok == false
16 tests: 16 passed, 0 failed$ soma verify docking_approach.cell
Verifying docking_approach.cell...
State machine 'approach': 6 states, initial 'station_keeping'
States: [aborted, captured, corridor, final_approach, hard_dock_complete, station_keeping]
✓ 6 states, 6 transitions
✓ all states reachable from 'station_keeping'
✓ terminal states: [hard_dock_complete, aborted]
✓ no deadlocks
✓ liveness: every state can eventually reach a terminal state
✓ think-isolated: CTL safety properties hold regardless of LLM output (5 handlers, 5 literal transitions, 0 dynamic)
✓ termination: all 6 handlers structurally terminate
✓ refinement: handler `begin_approach` ⟶ {corridor}
✓ refinement: handler `go_final` ⟶ {final_approach}
✓ refinement: handler `soft_capture` ⟶ {captured}
✓ refinement: handler `hard_dock` ⟶ {hard_dock_complete}
✓ refinement: handler `abort_approach` ⟶ {aborted}
· transition sources: `begin_approach`, `go_final`, `soft_capture`, `hard_dock`, `abort_approach` call transition(id, target) without declaring the source — the runtime refuses a move the graph forbids, but the proof does not see where the handler starts; `transition(id, from, to)` names the edge (checked by `soma check`, refused at run time from any other state)
DockingApproach/invariants:
⚠ invariant closing_cm_s >= 0 && closing_cm_s <= 30 — runtime-checked (computed values): go_final → closing_cm_s because `closing_cm_s_v` is a parameter (narrow it: `require closing_cm_s_v >= 0 && closing_cm_s_v <= 30 else …`)
12 passed, 1 warnings, 0 failures
Temporal properties for 'approach':
✓ deadlock_free — no deadlocks in any reachable state
Temporal: 1 passed, 0 failed
VERIFY OK