Ev charging — no current before the handshake, no session past the cap
Two real failure modes, one charging post. 1. Current before negotiation. The J1772/CCS pilot handshake exists so the cable, the car, and the post agree on a current the weakest link can carry. Skip it — a bent pin, a counterfeit adapter — and a 16 A cable carries 48 A until the insulation melts. Garage fires from uncertified EVSE gear are a documented pattern. 2. Charging past the cell limit. GM's 2021 recall of every Chevy Bolt came down to defective cells that became dangerous near full charge; the interim fix was SOFTWARE: cap the session. A cap that lives in an if-statement is a cap a firmware update can delete. Both are structural here: `charging` is reachable ONLY through `authorized` (the handshake state — soma verify proves it), and the session energy ledger refuses any write past 60 kWh at the memory.
A Soma program from the energy part of the verified corpus: 124 lines, cells ChargePost, ChargeTests. It passes soma check with soma 3.2.4.
How it is built
The cell ChargePost declares 8 signals in its face: plug_in(session: String) -> Map, handshake_done(session: String) -> Map, start_charge(session: String) -> Map, add_energy(session: String, kwh: Int) -> Map, stop_charge(session: String) -> Map, unplug(session: String) -> Map, session_state(session: String) -> String and delivered_kwh(session: String) -> Int; keeps 1 memory slot: delivered (Map<String, Int>, persistent and consistent) under the invariant delivered >= 0 && delivered <= 60. Its state machine session_fsm starts in idle and allows 5 transitions: idle -> plugged, plugged -> authorized, authorized -> charging, charging -> finished and finished -> idle. Handlers: plug_in (line 49), handshake_done (line 55), start_charge (line 61), add_energy (line 67), stop_charge (line 80), unplug (line 86), session_state (line 92) and delivered_kwh (line 93).
The file also holds 1 test cell (ChargeTests) with 15 assertions, run by soma test.
The source
ev_charging.cell
// ============================================================================
// EV CHARGING — no current before the handshake, no session past the cap
// ============================================================================
//
// Two real failure modes, one charging post.
//
// 1. Current before negotiation. The J1772/CCS pilot handshake exists so
// the cable, the car, and the post agree on a current the weakest link
// can carry. Skip it — a bent pin, a counterfeit adapter — and a 16 A
// cable carries 48 A until the insulation melts. Garage fires from
// uncertified EVSE gear are a documented pattern.
// 2. Charging past the cell limit. GM's 2021 recall of every Chevy Bolt
// came down to defective cells that became dangerous near full charge;
// the interim fix was SOFTWARE: cap the session. A cap that lives in
// an if-statement is a cap a firmware update can delete.
//
// Both are structural here: `charging` is reachable ONLY through
// `authorized` (the handshake state — soma verify proves it), and the
// session energy ledger refuses any write past 60 kWh at the memory.
// ============================================================================
cell ChargePost {
face {
signal plug_in(session: String) -> Map
signal handshake_done(session: String) -> Map
signal start_charge(session: String) -> Map
signal add_energy(session: String, kwh: Int) -> Map
signal stop_charge(session: String) -> Map
signal unplug(session: String) -> Map
signal session_state(session: String) -> String
signal delivered_kwh(session: String) -> Int
}
memory {
// kWh delivered this session — the cap is the wall
delivered: Map<String, Int> [persistent, consistent]
invariant delivered >= 0 && delivered <= 60
}
state session_fsm {
initial: idle
idle -> plugged // cable seated, pilot signal present
plugged -> authorized // handshake: current limit + payment agreed
authorized -> charging // contactor closes
charging -> finished // target reached or user stop
finished -> idle // unplug
}
on plug_in(session: String) {
let r = try { transition(session, "plugged") }
if r.error != () { return map("ok", false, "reason", "{r.error}") }
return map("ok", true, "state", get_status(session))
}
on handshake_done(session: String) {
let r = try { transition(session, "authorized") }
if r.error != () { return map("ok", false, "reason", "{r.error}") }
return map("ok", true, "state", get_status(session))
}
on start_charge(session: String) {
let r = try { transition(session, "charging") }
if r.error != () { return map("ok", false, "reason", "{r.error}") }
return map("ok", true, "state", get_status(session))
}
on add_energy(session: String, kwh: Int) {
if kwh <= 0 { return map("ok", false, "reason", "energy must be positive") }
if get_status(session) != "charging" {
return map("ok", false, "reason", "contactor open (state: {get_status(session)})")
}
let total = delivered_kwh(session) + kwh
let w = try { delivered.set(session, total) }
if w.error != () {
return map("ok", false, "reason", "session cap: {w.error}")
}
return map("ok", true, "delivered", total)
}
on stop_charge(session: String) {
let r = try { transition(session, "finished") }
if r.error != () { return map("ok", false, "reason", "{r.error}") }
return map("ok", true, "state", get_status(session))
}
on unplug(session: String) {
let r = try { transition(session, "idle") }
if r.error != () { return map("ok", false, "reason", "{r.error}") }
return map("ok", true, "state", get_status(session))
}
on session_state(session: String) { return get_status(session) }
on delivered_kwh(session: String) { return delivered.get(session) ?? 0 }
}
cell test ChargeTests {
rules {
// plug in — but current BEFORE the handshake does not exist
assert plug_in("s1").ok == true
assert_fails transition("s1", "charging")
assert add_energy("s1", 5).ok == false // contactor stays open
// handshake, then charge
assert handshake_done("s1").ok == true
assert start_charge("s1").ok == true
assert add_energy("s1", 30).ok == true
assert add_energy("s1", 25).ok == true // 55 kWh
// the interval that would cross the cap is refused; ledger intact
assert add_energy("s1", 10).ok == false // 55 + 10 > 60
assert delivered_kwh("s1") == 55
// THE WALL itself, with every handler check deleted
assert_fails delivered.set("s1", 90)
assert_fails delivered.set("s1", -2)
// orderly finish
assert stop_charge("s1").ok == true
assert add_energy("s1", 1).ok == false // no energy after stop
assert unplug("s1").ok == true
assert session_state("s1") == "idle"
}
}What the toolchain says
$ soma check ev_charging.cell
✓ All checks passed.$ soma test ev_charging.cell
test ChargeTests ...
✓ assert plug_in("s1").ok == true
✓ assert_fails transition("s1", "charging") — raised invalid transition: plugged → charging. Current state: 'plugged'. Valid targets: [authorized]
✓ assert add_energy("s1", 5).ok == false
✓ assert handshake_done("s1").ok == true
✓ assert start_charge("s1").ok == true
✓ assert add_energy("s1", 30).ok == true
✓ assert add_energy("s1", 25).ok == true
✓ assert add_energy("s1", 10).ok == false
✓ assert delivered_kwh("s1") == 55
✓ assert_fails delivered.set("s1", 90) — raised memory invariant violated on 'delivered': delivered >= 0 && delivered <= 60 — rejected write of 90 (key "s1"); the slot is unchanged
✓ assert_fails delivered.set("s1", -2) — raised memory invariant violated on 'delivered': delivered >= 0 && delivered <= 60 — rejected write of -2 (key "s1"); the slot is unchanged
✓ assert stop_charge("s1").ok == true
✓ assert add_energy("s1", 1).ok == false
✓ assert unplug("s1").ok == true
✓ assert session_state("s1") == "idle"
15 tests: 15 passed, 0 failed$ soma verify ev_charging.cell
Verifying ev_charging.cell...
State machine 'session_fsm': 5 states, initial 'idle'
States: [authorized, charging, finished, idle, plugged]
✓ 5 states, 5 transitions
✓ all states reachable from 'idle'
✓ reactive machine: no terminal state, and every reachable state can return to 'idle'
✓ no deadlocks
✓ liveness: every reachable state can return to 'idle' (reactive machine)
✓ think-isolated: CTL safety properties hold regardless of LLM output (5 handlers, 5 literal transitions, 0 dynamic)
✓ termination: all 8 handlers structurally terminate
✓ refinement: handler `plug_in` ⟶ {plugged}
✓ refinement: handler `handshake_done` ⟶ {authorized}
✓ refinement: handler `start_charge` ⟶ {charging}
✓ refinement: handler `stop_charge` ⟶ {finished}
✓ refinement: handler `unplug` ⟶ {idle}
· transition sources: `plug_in`, `handshake_done`, `start_charge`, `stop_charge`, `unplug` 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)
ChargePost/invariants:
✓ invariant delivered >= 0 — writer 'add_energy' proven by induction (writes total)
⚠ invariant delivered >= 0 && delivered <= 60 — runtime-checked (computed values): add_energy → delivered [delivered <= 60] because `total` is only known to lie in [1, ∞) — narrow it: `require total <= 60 else …`
13 passed, 1 warnings, 0 failures
Temporal properties for 'session_fsm':
✓ deadlock_free — no deadlocks in any reachable state
Temporal: 1 passed, 0 failed
VERIFY OK