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