soma.cell

Pipeline pig trap — the closure never opens against trapped pressure

Pig launcher/receiver closures keep killing pipeliners the same way: the trap is valved off from the line, the gauge "reads zero" (or nobody looks), the yoke is swung — and residual gas behind the pig fires the closure door like a cannon breech. Industry recommended practice is a hard sequence: isolate, blow down, PROVE zero, then open. Here that sequence is the only path that exists: `in_service` and `isolated` have no edge to `door_open`; only `blown_down` does, and the blowdown edge zeroes the trap before the door edge appears. The trap gauge ledger refuses any write past the 6000 kPa rating.

A Soma program from the safety_interlocks part of the verified corpus: 99 lines, cells PigTrap, PigTrapTests. It passes soma check with soma 3.2.4.

How it is built

The cell PigTrap declares 6 signals in its face: isolate_trap(id: String) -> String, return_to_service(id: String, kpa: Int) -> Map, blow_down(id: String) -> String, open_closure(id: String) -> Map, shut_closure(id: String) -> String and trap_kpa(id: String) -> Int; keeps 1 memory slot: trap_gauge (Map<String, Int>, persistent and consistent) under the invariant trap_gauge >= 0 && trap_gauge <= 6000. Its state machine trap starts in in_service and allows 6 transitions: in_service -> isolated, isolated -> in_service, isolated -> blown_down, blown_down -> door_open, door_open -> blown_down and blown_down -> isolated. Handlers: isolate_trap (line 38), return_to_service (line 40), blow_down (line 53), open_closure (line 59), shut_closure (line 68) and trap_kpa (line 69).

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

Generated from soma describe on this file.

The source

pig_trap_closure.cell

// PIPELINE PIG TRAP — the closure never opens against trapped pressure.
// Pig launcher/receiver closures keep killing pipeliners the same way:
// the trap is valved off from the line, the gauge "reads zero" (or
// nobody looks), the yoke is swung — and residual gas behind the pig
// fires the closure door like a cannon breech. Industry recommended
// practice is a hard sequence: isolate, blow down, PROVE zero, then
// open. Here that sequence is the only path that exists: `in_service`
// and `isolated` have no edge to `door_open`; only `blown_down` does,
// and the blowdown edge zeroes the trap before the door edge appears.
// The trap gauge ledger refuses any write past the 6000 kPa rating.

cell PigTrap {
    face {
        signal isolate_trap(id: String) -> String
        signal return_to_service(id: String, kpa: Int) -> Map
        signal blow_down(id: String) -> String
        signal open_closure(id: String) -> Map
        signal shut_closure(id: String) -> String
        signal trap_kpa(id: String) -> Int
    }

    memory {
        // trap pressure, kPa gauge; the barrel is rated to 6000
        trap_gauge: Map<String, Int> [persistent, consistent]
        invariant trap_gauge >= 0 && trap_gauge <= 6000
    }

    state trap {
        initial: in_service
        in_service -> isolated      // kicker and isolation valves closed
        isolated -> in_service      // back into the line, never opened
        isolated -> blown_down      // vented to flare, gauge proven zero
        blown_down -> door_open     // the ONLY edge to an open closure
        door_open -> blown_down     // door shut and yoked
        blown_down -> isolated      // re-pressurization starts from sealed
    }

    on isolate_trap(id: String) { transition(id, "isolated")  return get_status(id) }

    on return_to_service(id: String, kpa: Int) {
        let r = try { transition(id, "in_service") }
        if r.error != () {
            return map("ok", false, "reason", "closure not proven shut and sealed")
        }
        let w = try { trap_gauge.set(id, kpa) }
        if w.error != () {
            transition(id, "isolated")
            return map("ok", false, "reason", "{kpa} kPa exceeds the 6000 kPa rating")
        }
        return map("ok", true, "state", get_status(id), "kpa", kpa)
    }

    on blow_down(id: String) {
        trap_gauge.set(id, 0)           // proven zero BEFORE the door can exist
        transition(id, "blown_down")
        return get_status(id)
    }

    on open_closure(id: String) {
        let r = try { transition(id, "door_open") }
        if r.error != () {
            let p = trap_kpa(id)
            return map("ok", false, "reason", "trap not blown down ({p} kPa) — yoke stays locked")
        }
        return map("ok", true, "state", get_status(id))
    }

    on shut_closure(id: String) { transition(id, "blown_down")  return get_status(id) }
    on trap_kpa(id: String) { return trap_gauge.get(id) ?? 0 }
}

cell test PigTrapTests {
    rules {
        // opening a live trap: no edge, refused
        assert open_closure("t1").ok == false
        assert_fails transition("t1", "door_open")

        // isolated is NOT zero — the breech stays locked
        assert isolate_trap("t1") == "isolated"
        assert open_closure("t1").ok == false

        // blow down, prove zero, and the door exists
        assert blow_down("t1") == "blown_down"
        assert trap_kpa("t1") == 0
        assert open_closure("t1").ok == true

        // an open trap cannot rejoin the line
        assert_fails transition("t1", "in_service")
        assert shut_closure("t1") == "blown_down"

        // back to service through the sealed ladder; the gauge is a wall
        assert isolate_trap("t1") == "isolated"
        assert return_to_service("t1", 9000).ok == false
        assert_fails trap_gauge.set("t1", 6001)
        assert return_to_service("t1", 4800).ok == true
        assert trap_kpa("t1") == 4800
    }
}

What the toolchain says

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

$ soma check pig_trap_closure.cell
✓ All checks passed.
$ soma test pig_trap_closure.cell
test PigTrapTests ...
  ✓ assert open_closure("t1").ok == false
  ✓ assert_fails transition("t1", "door_open") — raised invalid transition: in_service → door_open. Current state: 'in_service'. Valid targets: [isolated]
  ✓ assert isolate_trap("t1") == "isolated"
  ✓ assert open_closure("t1").ok == false
  ✓ assert blow_down("t1") == "blown_down"
  ✓ assert trap_kpa("t1") == 0
  ✓ assert open_closure("t1").ok == true
  ✓ assert_fails transition("t1", "in_service") — raised invalid transition: door_open → in_service. Current state: 'door_open'. Valid targets: [blown_down]
  ✓ assert shut_closure("t1") == "blown_down"
  ✓ assert isolate_trap("t1") == "isolated"
  ✓ assert return_to_service("t1", 9000).ok == false
  ✓ assert_fails trap_gauge.set("t1", 6001) — raised memory invariant violated on 'trap_gauge': trap_gauge >= 0 && trap_gauge <= 6000 — rejected write of 6001 (key "t1"); the slot is unchanged
  ✓ assert return_to_service("t1", 4800).ok == true
  ✓ assert trap_kpa("t1") == 4800

14 tests: 14 passed, 0 failed
$ soma verify pig_trap_closure.cell
Verifying pig_trap_closure.cell...

State machine 'trap': 4 states, initial 'in_service'
  States: [blown_down, door_open, in_service, isolated]

  ✓ 4 states, 6 transitions
  ✓ all states reachable from 'in_service'
  ✓ reactive machine: no terminal state, and every reachable state can return to 'in_service'
  ✓ no deadlocks
  ✓ liveness: every reachable state can return to 'in_service' (reactive machine)
  ✓ think-isolated: CTL safety properties hold regardless of LLM output (5 handlers, 6 literal transitions, 0 dynamic)
  ✓ termination: all 6 handlers structurally terminate
  ✓ refinement: handler `isolate_trap` ⟶ {isolated}
  ✓ refinement: handler `return_to_service` ⟶ {in_service, isolated [if w.error != ()]}
  ✓ refinement: handler `blow_down` ⟶ {blown_down}
  ✓ refinement: handler `open_closure` ⟶ {door_open}
  ✓ refinement: handler `shut_closure` ⟶ {blown_down}
  · transition sources: `isolate_trap`, `return_to_service`, `blow_down`, `open_closure`, `shut_closure` 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)

PigTrap/invariants:

  ✓ invariant trap_gauge >= 0 && trap_gauge <= 6000 — writer 'blow_down' proven (writes 0)
  ⚠ invariant trap_gauge >= 0 && trap_gauge <= 6000 — runtime-checked (computed values): return_to_service → trap_gauge because `kpa` is a parameter (narrow it: `require kpa >= 0 && kpa <= 6000 else …`)

13 passed, 1 warnings, 0 failures

  Temporal properties for 'trap':
  ✓ deadlock_free — no deadlocks in any reachable state
Temporal: 1 passed, 0 failed
VERIFY OK

Builtins used

get_status, map, transition

Source file: /corpus/safety_interlocks/pig_trap_closure.cell · examples/corpus/safety_interlocks/pig_trap_closure.cell