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

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.

Generated from soma describe on this file.

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

Run on a fresh copy of the program at build time; the outputs are pasted unedited.

$ 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

Builtins used

get_status, map, transition

Source file: /corpus/aerospace/docking_approach.cell · examples/corpus/aerospace/docking_approach.cell