Chlorine dosing — no chemical feed without proven water flow
The CDC logs hundreds of poolgoers hospitalized in chemical-controller incidents with one recurring shape: the recirculation pump stops, the chlorine feeder keeps running, and the stagnant line accumulates a slug that gasses the first swimmers when flow returns. The fix is a flow interlock: the feeder is DOWNSTREAM of proven flow in the state machine itself — `standby -> feeding` does not exist, and losing flow while feeding forces the feed-stop edge before the pump-stop edge. The residual ledger independently refuses any dose past the 4 ppm EPA maximum residual disinfectant level.
A Soma program from the safety_interlocks part of the verified corpus: 88 lines, cells ChlorineFeed, FeedTests. It passes soma check with soma 3.2.4.
How it is built
The cell ChlorineFeed declares 5 signals in its face: flow_proven(id: String) -> String, start_feed(id: String, ppm: Int) -> Map, stop_feed(id: String) -> String, flow_lost(id: String) -> String and residual_of(id: String) -> Int; keeps 1 memory slot: residual (Map<String, Int>, persistent and consistent) under the invariant residual >= 0 && residual <= 4. Its state machine feeder starts in standby and allows 4 transitions: standby -> circulating, circulating -> standby, circulating -> feeding and feeding -> circulating. Handlers: flow_proven (line 35), start_feed (line 37), stop_feed (line 50), flow_lost (line 52) and residual_of (line 59).
The file also holds 1 test cell (FeedTests) with 12 assertions, run by soma test.
The source
chlorine_dosing.cell
// CHLORINE DOSING — no chemical feed without proven water flow.
// The CDC logs hundreds of poolgoers hospitalized in chemical-controller
// incidents with one recurring shape: the recirculation pump stops, the
// chlorine feeder keeps running, and the stagnant line accumulates a
// slug that gasses the first swimmers when flow returns. The fix is a
// flow interlock: the feeder is DOWNSTREAM of proven flow in the state
// machine itself — `standby -> feeding` does not exist, and losing flow
// while feeding forces the feed-stop edge before the pump-stop edge.
// The residual ledger independently refuses any dose past the 4 ppm
// EPA maximum residual disinfectant level.
cell ChlorineFeed {
face {
signal flow_proven(id: String) -> String
signal start_feed(id: String, ppm: Int) -> Map
signal stop_feed(id: String) -> String
signal flow_lost(id: String) -> String
signal residual_of(id: String) -> Int
}
memory {
// chlorine residual at the sensor, ppm x1 — EPA MRDL is 4
residual: Map<String, Int> [persistent, consistent]
invariant residual >= 0 && residual <= 4
}
state feeder {
initial: standby
standby -> circulating // flow switch made: water is moving
circulating -> standby // pump stopped while not feeding
circulating -> feeding // the ONLY edge into chemical feed
feeding -> circulating // feed stops FIRST, flow continues
}
on flow_proven(id: String) { transition(id, "circulating") return get_status(id) }
on start_feed(id: String, ppm: Int) {
let r = try { transition(id, "feeding") }
if r.error != () {
return map("ok", false, "reason", "no proven flow — feeder stays off")
}
let w = try { residual.set(id, ppm) }
if w.error != () {
transition(id, "circulating")
return map("ok", false, "reason", "{ppm} ppm exceeds the 4 ppm MRDL")
}
return map("ok", true, "state", get_status(id), "ppm", ppm)
}
on stop_feed(id: String) { transition(id, "circulating") return get_status(id) }
on flow_lost(id: String) {
// flow failure: kill the feed, THEN the line may go quiet
if get_status(id) == "feeding" { transition(id, "circulating") }
transition(id, "standby")
return get_status(id)
}
on residual_of(id: String) { return residual.get(id) ?? 0 }
}
cell test FeedTests {
rules {
// feeding into a dead line: the edge is absent
assert_fails transition("pool1", "feeding")
assert start_feed("pool1", 2).ok == false
// proven flow, lawful dose
assert flow_proven("pool1") == "circulating"
assert start_feed("pool1", 3).ok == true
assert residual_of("pool1") == 3
// a feeding line cannot go straight to standby — feed stops first
assert_fails transition("pool1", "standby")
assert flow_lost("pool1") == "standby"
// the residual ledger refuses an overdose, structurally
assert flow_proven("pool1") == "circulating"
assert start_feed("pool1", 9).ok == false
assert_fails residual.set("pool1", 5)
assert_fails residual.set("pool1", -1)
// and the feeder fell back to circulating, not feeding — a
// normal pump stop from here is lawful
assert flow_lost("pool1") == "standby"
}
}What the toolchain says
$ soma check chlorine_dosing.cell
✓ All checks passed.$ soma test chlorine_dosing.cell
test FeedTests ...
✓ assert_fails transition("pool1", "feeding") — raised invalid transition: standby → feeding. Current state: 'standby'. Valid targets: [circulating]
✓ assert start_feed("pool1", 2).ok == false
✓ assert flow_proven("pool1") == "circulating"
✓ assert start_feed("pool1", 3).ok == true
✓ assert residual_of("pool1") == 3
✓ assert_fails transition("pool1", "standby") — raised invalid transition: feeding → standby. Current state: 'feeding'. Valid targets: [circulating]
✓ assert flow_lost("pool1") == "standby"
✓ assert flow_proven("pool1") == "circulating"
✓ assert start_feed("pool1", 9).ok == false
✓ assert_fails residual.set("pool1", 5) — raised memory invariant violated on 'residual': residual >= 0 && residual <= 4 — rejected write of 5 (key "pool1"); the slot is unchanged
✓ assert_fails residual.set("pool1", -1) — raised memory invariant violated on 'residual': residual >= 0 && residual <= 4 — rejected write of -1 (key "pool1"); the slot is unchanged
✓ assert flow_lost("pool1") == "standby"
12 tests: 12 passed, 0 failed$ soma verify chlorine_dosing.cell
Verifying chlorine_dosing.cell...
State machine 'feeder': 3 states, initial 'standby'
States: [circulating, feeding, standby]
✓ 3 states, 4 transitions
✓ all states reachable from 'standby'
✓ reactive machine: no terminal state, and every reachable state can return to 'standby'
✓ no deadlocks
✓ liveness: every reachable state can return to 'standby' (reactive machine)
✓ think-isolated: CTL safety properties hold regardless of LLM output (4 handlers, 6 literal transitions, 0 dynamic)
✓ termination: all 5 handlers structurally terminate
✓ refinement: handler `flow_proven` ⟶ {circulating}
✓ refinement: handler `start_feed` ⟶ {feeding, circulating [if w.error != ()]}
✓ refinement: handler `stop_feed` ⟶ {circulating}
✓ refinement: handler `flow_lost` ⟶ {feeding → circulating [if get_status(id) == "feeding"], standby}
· transition sources: `flow_proven`, `start_feed`, `stop_feed`, `flow_lost` 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)
ChlorineFeed/invariants:
⚠ invariant residual >= 0 && residual <= 4 — runtime-checked (computed values): start_feed → residual because `ppm` is a parameter (narrow it: `require ppm >= 0 && ppm <= 4 else …`)
11 passed, 1 warnings, 0 failures
Temporal properties for 'feeder':
✓ deadlock_free — no deadlocks in any reachable state
Temporal: 1 passed, 0 failed
VERIFY OK