Lockout-tagout — test before touch, and nobody re-energizes over a lock
Electrical workers are killed every year by circuits that were "locked out": a breaker opened but the bus still live through a backfeed, an induced voltage from the parallel line nobody tested for, or a dispatcher closing remotely while a crew is on the conductors. OSHA's hazardous- energy rules (1910.147 / 1910.269) exist because each of these has a body count. The two rules that do the saving: TEST BEFORE TOUCH — isolation is not proof. You meter every phase before grounds go on. Here, `grounded` is unreachable from `isolated` without passing `proven_dead`; soma verify proves it. ONE LOCK, ONE LIFE — every worker hangs a personal lock; the line cannot be declared clear while ANY lock remains. The lock ledger refuses to go negative (you cannot remove a lock that isn't yours to remove), and re-energizing from `grounded` is not a transition at all.
A Soma program from the energy part of the verified corpus: 137 lines, cells Loto, LotoTests. It passes soma check with soma 3.2.4.
How it is built
The cell Loto declares 9 signals in its face: open_and_lock(bay: String) -> Map, meter_all_phases(bay: String) -> Map, apply_grounds(bay: String) -> Map, hang_lock(bay: String, worker: String) -> Map, pull_lock(bay: String, worker: String) -> Map, declare_clear(bay: String) -> Map, energize(bay: String) -> Map, bay_state(bay: String) -> String and lock_total(bay: String) -> Int; keeps 1 memory slot: locks (Map<String, Int>, persistent and consistent) under the invariant locks >= 0 && locks <= 20. Its state machine clearance starts in energized and allows 5 transitions: energized -> isolated, isolated -> proven_dead, proven_dead -> grounded, grounded -> clear and clear -> energized. Handlers: open_and_lock (line 50), meter_all_phases (line 56), apply_grounds (line 62), hang_lock (line 68), pull_lock (line 74), declare_clear (line 82), energize (line 92), bay_state (line 98) and lock_total (line 99).
The file also holds 1 test cell (LotoTests) with 18 assertions, run by soma test.
The source
substation_loto.cell
// ============================================================================
// LOCKOUT-TAGOUT — test before touch, and nobody re-energizes over a lock
// ============================================================================
//
// Electrical workers are killed every year by circuits that were "locked
// out": a breaker opened but the bus still live through a backfeed, an
// induced voltage from the parallel line nobody tested for, or a dispatcher
// closing remotely while a crew is on the conductors. OSHA's hazardous-
// energy rules (1910.147 / 1910.269) exist because each of these has a
// body count. The two rules that do the saving:
//
// TEST BEFORE TOUCH — isolation is not proof. You meter every phase
// before grounds go on. Here, `grounded` is unreachable from `isolated`
// without passing `proven_dead`; soma verify proves it.
//
// ONE LOCK, ONE LIFE — every worker hangs a personal lock; the line
// cannot be declared clear while ANY lock remains. The lock ledger
// refuses to go negative (you cannot remove a lock that isn't yours to
// remove), and re-energizing from `grounded` is not a transition at all.
// ============================================================================
cell Loto {
face {
signal open_and_lock(bay: String) -> Map
signal meter_all_phases(bay: String) -> Map
signal apply_grounds(bay: String) -> Map
signal hang_lock(bay: String, worker: String) -> Map
signal pull_lock(bay: String, worker: String) -> Map
signal declare_clear(bay: String) -> Map
signal energize(bay: String) -> Map
signal bay_state(bay: String) -> String
signal lock_total(bay: String) -> Int
}
memory {
// personal locks hanging on the bay — one per worker, max crew 20
locks: Map<String, Int> [persistent, consistent]
invariant locks >= 0 && locks <= 20
}
state clearance {
initial: energized
energized -> isolated // breaker open, switch racked, tag hung
isolated -> proven_dead // TEST BEFORE TOUCH: metered every phase
proven_dead -> grounded // ground clusters on — work may begin
grounded -> clear // work done, grounds OFF, all locks pulled
clear -> energized
}
on open_and_lock(bay: String) {
let r = try { transition(bay, "isolated") }
if r.error != () { return map("ok", false, "reason", "{r.error}") }
return map("ok", true, "state", get_status(bay))
}
on meter_all_phases(bay: String) {
let r = try { transition(bay, "proven_dead") }
if r.error != () { return map("ok", false, "reason", "{r.error}") }
return map("ok", true, "state", get_status(bay))
}
on apply_grounds(bay: String) {
let r = try { transition(bay, "grounded") }
if r.error != () { return map("ok", false, "reason", "{r.error}") }
return map("ok", true, "state", get_status(bay))
}
on hang_lock(bay: String, worker: String) {
let w = try { locks.set(bay, lock_total(bay) + 1) }
if w.error != () { return map("ok", false, "reason", "{w.error}") }
return map("ok", true, "worker", worker, "locks", lock_total(bay))
}
on pull_lock(bay: String, worker: String) {
let w = try { locks.set(bay, lock_total(bay) - 1) }
if w.error != () {
return map("ok", false, "reason", "no lock to pull — the ledger refuses")
}
return map("ok", true, "worker", worker, "locks", lock_total(bay))
}
on declare_clear(bay: String) {
if lock_total(bay) > 0 {
return map("ok", false, "reason",
"{lock_total(bay)} personal lock(s) still hanging — someone is on the line")
}
let r = try { transition(bay, "clear") }
if r.error != () { return map("ok", false, "reason", "{r.error}") }
return map("ok", true, "state", get_status(bay))
}
on energize(bay: String) {
let r = try { transition(bay, "energized") }
if r.error != () { return map("ok", false, "reason", "{r.error}") }
return map("ok", true, "state", get_status(bay))
}
on bay_state(bay: String) { return get_status(bay) }
on lock_total(bay: String) { return locks.get(bay) ?? 0 }
}
cell test LotoTests {
rules {
// isolate the bay; two workers hang their locks
assert open_and_lock("bay7").ok == true
assert hang_lock("bay7", "ana").ok == true
assert hang_lock("bay7", "raj").ok == true
assert lock_total("bay7") == 2
// SKIPPING THE DEAD TEST: grounds straight onto an untested bus.
// The transition does not exist.
assert_fails transition("bay7", "grounded")
assert meter_all_phases("bay7").ok == true
assert apply_grounds("bay7").ok == true
// RE-ENERGIZING OVER THE CREW: not a transition from grounded
assert_fails transition("bay7", "energized")
assert energize("bay7").ok == false
// clear is blocked while ANY personal lock remains
assert declare_clear("bay7").ok == false
assert pull_lock("bay7", "ana").ok == true
assert declare_clear("bay7").ok == false // raj is still on the line
assert pull_lock("bay7", "raj").ok == true
// a third pull has no lock behind it — the ledger raises
assert_fails locks.set("bay7", -1)
assert pull_lock("bay7", "ghost").ok == false
// now, and only now, the bay comes back
assert declare_clear("bay7").ok == true
assert energize("bay7").ok == true
assert bay_state("bay7") == "energized"
}
}What the toolchain says
$ soma check substation_loto.cell
✓ All checks passed.$ soma test substation_loto.cell
test LotoTests ...
✓ assert open_and_lock("bay7").ok == true
✓ assert hang_lock("bay7", "ana").ok == true
✓ assert hang_lock("bay7", "raj").ok == true
✓ assert lock_total("bay7") == 2
✓ assert_fails transition("bay7", "grounded") — raised invalid transition: isolated → grounded. Current state: 'isolated'. Valid targets: [proven_dead]
✓ assert meter_all_phases("bay7").ok == true
✓ assert apply_grounds("bay7").ok == true
✓ assert_fails transition("bay7", "energized") — raised invalid transition: grounded → energized. Current state: 'grounded'. Valid targets: [clear]
✓ assert energize("bay7").ok == false
✓ assert declare_clear("bay7").ok == false
✓ assert pull_lock("bay7", "ana").ok == true
✓ assert declare_clear("bay7").ok == false
✓ assert pull_lock("bay7", "raj").ok == true
✓ assert_fails locks.set("bay7", -1) — raised memory invariant violated on 'locks': locks >= 0 && locks <= 20 — rejected write of -1 (key "bay7"); the slot is unchanged
✓ assert pull_lock("bay7", "ghost").ok == false
✓ assert declare_clear("bay7").ok == true
✓ assert energize("bay7").ok == true
✓ assert bay_state("bay7") == "energized"
18 tests: 18 passed, 0 failed$ soma verify substation_loto.cell
Verifying substation_loto.cell...
State machine 'clearance': 5 states, initial 'energized'
States: [clear, energized, grounded, isolated, proven_dead]
✓ 5 states, 5 transitions
✓ all states reachable from 'energized'
✓ reactive machine: no terminal state, and every reachable state can return to 'energized'
✓ no deadlocks
✓ liveness: every reachable state can return to 'energized' (reactive machine)
✓ think-isolated: CTL safety properties hold regardless of LLM output (5 handlers, 5 literal transitions, 0 dynamic)
✓ termination: all 9 handlers structurally terminate
✓ refinement: handler `open_and_lock` ⟶ {isolated}
✓ refinement: handler `meter_all_phases` ⟶ {proven_dead}
✓ refinement: handler `apply_grounds` ⟶ {grounded}
✓ refinement: handler `declare_clear` ⟶ {clear}
✓ refinement: handler `energize` ⟶ {energized}
· transition sources: `open_and_lock`, `meter_all_phases`, `apply_grounds`, `declare_clear`, `energize` 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)
Loto/invariants:
✓ invariant locks >= 0 — writer 'hang_lock' proven (writes lock_total(bay) + 1)
✓ invariant locks <= 20 — writer 'pull_lock' proven (writes lock_total(bay) - 1)
⚠ invariant locks >= 0 && locks <= 20 — runtime-checked (computed values): hang_lock → locks [locks <= 20] because `lock_total(bay) + 1` is only known to lie in [1, 21] — narrow it: `require (lock_total(bay) + 1) <= 20 else …`, pull_lock → locks [locks >= 0] because `lock_total(bay) - 1` is only known to lie in [-1, 19] — narrow it: `require (lock_total(bay) - 1) >= 0 else …`
14 passed, 1 warnings, 0 failures
Temporal properties for 'clearance':
✓ deadlock_free — no deadlocks in any reachable state
Temporal: 1 passed, 0 failed
VERIFY OK