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

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.

Generated from soma describe on this file.

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

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

$ 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

Builtins used

get_status, map, transition

Source file: /corpus/safety_interlocks/chlorine_dosing.cell · examples/corpus/safety_interlocks/chlorine_dosing.cell