soma.cell

Autopilot authority — Aeroflot 593: the half-disconnected autopilot

On 23 March 1994, Aeroflot Flight 593 (an A310) crashed into a Siberian hillside, killing 75. The captain's children were at the controls; the boy's yoke force partially overrode the autopilot — it silently dropped the AILERON channel while the rest stayed engaged. No light, no horn. The crew flew for crucial seconds believing the autopilot had the aircraft. Nobody had authority, and nobody knew. The defect was authority as PER-AXIS FLAGS that could disagree. Here authority is ONE state. Yoke force past the breakout threshold drives the machine to override_latched — fully, loudly, all axes — and the only exit is an explicit crew acknowledgment. There is no edge from override_latched back to engaged: a fought autopilot can never silently take the aircraft back. A memory invariant caps servo torque so the actuator can never be commanded past its breakout rating.

A Soma program from the aerospace part of the verified corpus: 108 lines, cells AutopilotAuthority, AuthorityTests. It passes soma check with soma 3.2.4.

How it is built

The cell AutopilotAuthority declares 5 signals in its face: engage(id: String) -> Map, disengage(id: String) -> Map, yoke_input(id: String, force_n: Int) -> Map, acknowledge_override(id: String) -> Map and authority(id: String) -> String; keeps 1 memory slot: servo_nm (Map<String, Int>, persistent and consistent) under the invariant servo_nm >= 0 && servo_nm <= 30. Its state machine autopilot starts in disengaged and allows 4 transitions: disengaged -> engaged, engaged -> disengaged, engaged -> override_latched and override_latched -> disengaged. Handlers: engage (line 46), disengage (line 53), yoke_input (line 60), acknowledge_override (line 71) and authority (line 77).

The file also holds 1 test cell (AuthorityTests) with 12 assertions, run by soma test.

Generated from soma describe on this file.

The source

autopilot_authority.cell

// ============================================================================
// AUTOPILOT AUTHORITY — Aeroflot 593: the half-disconnected autopilot
// ============================================================================
//
// On 23 March 1994, Aeroflot Flight 593 (an A310) crashed into a Siberian
// hillside, killing 75. The captain's children were at the controls; the
// boy's yoke force partially overrode the autopilot — it silently dropped
// the AILERON channel while the rest stayed engaged. No light, no horn.
// The crew flew for crucial seconds believing the autopilot had the
// aircraft. Nobody had authority, and nobody knew.
//
// The defect was authority as PER-AXIS FLAGS that could disagree. Here
// authority is ONE state. Yoke force past the breakout threshold drives
// the machine to override_latched — fully, loudly, all axes — and the
// only exit is an explicit crew acknowledgment. There is no edge from
// override_latched back to engaged: a fought autopilot can never silently
// take the aircraft back. A memory invariant caps servo torque so the
// actuator can never be commanded past its breakout rating.
// ============================================================================

cell AutopilotAuthority {
    face {
        signal engage(id: String) -> Map
        signal disengage(id: String) -> Map
        signal yoke_input(id: String, force_n: Int) -> Map
        signal acknowledge_override(id: String) -> Map
        signal authority(id: String) -> String
    }

    memory {
        // commanded servo torque, N·m — breakout clutch rated at 30
        servo_nm: Map<String, Int> [persistent, consistent]
        invariant servo_nm >= 0 && servo_nm <= 30
    }

    // Authority is one state, never per-axis flags.
    // ABSENT: override_latched -> engaged (no silent re-engagement)
    state autopilot {
        initial: disengaged
        disengaged -> engaged
        engaged -> disengaged          // crew button: clean handover
        engaged -> override_latched    // yoke force: AP surrenders ALL axes
        override_latched -> disengaged // ONLY via explicit acknowledgment
    }

    on engage(id: String) {
        let r = try { transition(id, "engaged") }
        if r.error != () { return map("ok", false, "reason", "{r.error}") }
        servo_nm.set(id, 12)
        return map("ok", true, "authority", get_status(id))
    }

    on disengage(id: String) {
        let r = try { transition(id, "disengaged") }
        if r.error != () { return map("ok", false, "reason", "{r.error}") }
        servo_nm.set(id, 0)
        return map("ok", true, "authority", get_status(id))
    }

    on yoke_input(id: String, force_n: Int) {
        // below breakout force the AP holds; past it, authority transfers
        // COMPLETELY — the Aeroflot 593 half-state has no representation
        if get_status(id) == "engaged" && force_n > 15 {
            transition(id, "override_latched")
            servo_nm.set(id, 0)
            return map("ok", true, "authority", "override_latched", "warn", "AP OFF — pilot has ALL axes")
        }
        return map("ok", true, "authority", get_status(id))
    }

    on acknowledge_override(id: String) {
        let r = try { transition(id, "disengaged") }
        if r.error != () { return map("ok", false, "reason", "{r.error}") }
        return map("ok", true, "authority", get_status(id))
    }

    on authority(id: String) { return get_status(id) }
}

cell test AuthorityTests {
    rules {
        // engage from cold
        assert engage("ap1").ok == true
        assert authority("ap1") == "engaged"

        // light yoke pressure: the AP keeps the aircraft
        assert yoke_input("ap1", 5).authority == "engaged"

        // force past breakout: AP surrenders EVERYTHING, latched
        assert yoke_input("ap1", 40).authority == "override_latched"

        // THE Aeroflot 593 hazard: a silent re-engage. The edge is absent.
        assert_fails transition("ap1", "engaged")
        assert engage("ap1").ok == false

        // only an explicit acknowledgment clears the latch...
        assert acknowledge_override("ap1").ok == true
        assert authority("ap1") == "disengaged"
        // ...and then a deliberate engage is legal again
        assert engage("ap1").ok == true
        assert disengage("ap1").ok == true

        // the servo can never be commanded past its breakout clutch
        assert_fails servo_nm.set("ap1", 90)
        assert_fails servo_nm.set("ap1", -1)
    }
}

What the toolchain says

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

$ soma check autopilot_authority.cell
✓ All checks passed.
$ soma test autopilot_authority.cell
test AuthorityTests ...
  ✓ assert engage("ap1").ok == true
  ✓ assert authority("ap1") == "engaged"
  ✓ assert yoke_input("ap1", 5).authority == "engaged"
  ✓ assert yoke_input("ap1", 40).authority == "override_latched"
  ✓ assert_fails transition("ap1", "engaged") — raised invalid transition: override_latched → engaged. Current state: 'override_latched'. Valid targets: [disengaged]
  ✓ assert engage("ap1").ok == false
  ✓ assert acknowledge_override("ap1").ok == true
  ✓ assert authority("ap1") == "disengaged"
  ✓ assert engage("ap1").ok == true
  ✓ assert disengage("ap1").ok == true
  ✓ assert_fails servo_nm.set("ap1", 90) — raised memory invariant violated on 'servo_nm': servo_nm >= 0 && servo_nm <= 30 — rejected write of 90 (key "ap1"); the slot is unchanged
  ✓ assert_fails servo_nm.set("ap1", -1) — raised memory invariant violated on 'servo_nm': servo_nm >= 0 && servo_nm <= 30 — rejected write of -1 (key "ap1"); the slot is unchanged

12 tests: 12 passed, 0 failed
$ soma verify autopilot_authority.cell
Verifying autopilot_authority.cell...

State machine 'autopilot': 3 states, initial 'disengaged'
  States: [disengaged, engaged, override_latched]

  ✓ 3 states, 4 transitions
  ✓ all states reachable from 'disengaged'
  ✓ reactive machine: no terminal state, and every reachable state can return to 'disengaged'
  ✓ no deadlocks
  ✓ liveness: every reachable state can return to 'disengaged' (reactive machine)
  ✓ think-isolated: CTL safety properties hold regardless of LLM output (4 handlers, 4 literal transitions, 0 dynamic)
  ✓ termination: all 5 handlers structurally terminate
  ✓ refinement: handler `engage` ⟶ {engaged}
  ✓ refinement: handler `disengage` ⟶ {disengaged}
  ✓ refinement: handler `yoke_input` ⟶ {engaged → override_latched [if get_status(id) == "engaged" && force_n > 15]}
  ✓ refinement: handler `acknowledge_override` ⟶ {disengaged}
  · transition sources: `engage`, `disengage`, `acknowledge_override` 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)

AutopilotAuthority/invariants:

  ✓ invariant servo_nm >= 0 && servo_nm <= 30 — writer 'disengage' proven (writes 0)
  ✓ invariant servo_nm >= 0 && servo_nm <= 30 — writer 'engage' proven (writes 12)
  ✓ invariant servo_nm >= 0 && servo_nm <= 30 — writer 'yoke_input' proven (writes 0)

14 passed, 0 warnings, 0 failures

  Temporal properties for 'autopilot':
  ✓ 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/autopilot_authority.cell · examples/corpus/aerospace/autopilot_authority.cell