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

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.

Generated from soma describe on this file.

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

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

$ 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

Builtins used

get_status, map, transition

Source file: /corpus/energy/substation_loto.cell · examples/corpus/energy/substation_loto.cell