soma.cell

Payments approval — a whole service in one file: a ledger that cannot overdraw, a payment lifecycle the verifier proves, an HTTP API that maps error kinds to status codes, and tests that run offline

What is PROVEN (soma verify + soma.toml): paid is unreachable without authorized; nothing follows rejected; every payment ends paid or rejected. What is ENFORCED: the balance never goes negative (invariant, checked on every write); a handler that fails is rolled back entirely, so a payment whose debit is refused stays exactly where it was.

A Soma program from the services part of the verified corpus: 165 lines, cells Ledger, Payments, PaymentsTests. It passes soma check with soma 3.2.4.

How it is built

The cell Ledger declares 3 signals in its face: _deposit(amount: Int) -> Int, _debit(amount: Int) -> Int and balance() -> Int; keeps 1 memory slot: account (Map<String, Int>, persistent) under the invariant account >= 0. Handlers: balance (line 26). Private helpers: _deposit and _debit.

The cell Payments declares 7 signals in its face: propose(id: String, amount: Int) -> Map, authorize(id: String) -> Map, sign_off(id: String, officer: String) -> Map, settle(id: String) -> Map, reject(id: String) -> Map, payment(id: String) -> Map and request(method: String, path: String, body: String) -> Map; keeps 1 memory slot: amounts (Map<String, Int>, persistent) under the invariant amounts > 0 && amounts <= 1000000. Its state machine flow starts in proposed and allows 7 transitions: proposed -> authorized, proposed -> pending_signoff, pending_signoff -> signed_off, signed_off -> authorized, authorized -> paid, proposed -> rejected and pending_signoff -> rejected. paid and rejected are terminal. Handlers: propose (line 66), authorize (line 71), sign_off (line 80), settle (line 86), reject (line 94), payment (line 98) and request (line 110). Private helpers: _amount and _route.

The file also holds 1 test cell (PaymentsTests) with 21 assertions, run by soma test.

Generated from soma describe on this file.

The source

soma.toml

[package]
name = "services-corpus"
version = "0.1.0"
description = "Whole verified services: HTTP + lifecycle + invariants, in one file each"

[agent]
mock = "echo"

# payments_approval.cell — the properties its README promises
[verify]
cells = ["Payments"]
eventually = ["paid", "rejected"]

[verify.before.paid]
requires = ["authorized"]

[verify.after.rejected]
never = ["paid", "authorized", "signed_off"]

[verify.after.paid]
never = ["rejected"]

payments_approval.cell

// PAYMENTS APPROVAL — a whole service in one file: a ledger that cannot
// overdraw, a payment lifecycle the verifier proves, an HTTP API that maps
// error kinds to status codes, and tests that run offline.
//
// What is PROVEN (soma verify + soma.toml): paid is unreachable without
// authorized; nothing follows rejected; every payment ends paid or rejected.
// What is ENFORCED: the balance never goes negative (invariant, checked on
// every write); a handler that fails is rolled back entirely, so a payment
// whose debit is refused stays exactly where it was.
//
//   soma check  payments_approval.cell
//   soma verify payments_approval.cell
//   soma test   payments_approval.cell
//   soma run    payments_approval.cell request POST /payments '{"id":"p1","amount":250}'

cell Ledger {
    face {
        signal _deposit(amount: Int) -> Int
        signal _debit(amount: Int) -> Int
        signal balance() -> Int
    }
    memory {
        account: Map<String, Int> [persistent]
        invariant account >= 0                      // an overdraft is unrepresentable
    }
    on balance() { return account.get("cash") ?? 0 }
    on _deposit(amount: Int) {
        require amount > 0 else InvalidAmount
        account.set("cash", balance() + amount)
        return balance()
    }
    on _debit(amount: Int) {
        require amount > 0 else InvalidAmount
        account.set("cash", balance() - amount)     // raises kind "invariant" when it would overdraw
        return balance()
    }
}

cell Payments {
    face {
        signal propose(id: String, amount: Int) -> Map
        signal authorize(id: String) -> Map
        signal sign_off(id: String, officer: String) -> Map
        signal settle(id: String) -> Map
        signal reject(id: String) -> Map
        signal payment(id: String) -> Map
        signal request(method: String, path: String, body: String) -> Map
    }
    memory {
        amounts: Map<String, Int> [persistent]
        invariant amounts > 0 && amounts <= 1000000
    }
    // Small payments: proposed -> authorized -> paid.
    // Large ones need a second person: proposed -> pending_signoff -> signed_off -> authorized.
    state flow {
        initial: proposed
        proposed -> authorized
        proposed -> pending_signoff
        pending_signoff -> signed_off
        signed_off -> authorized
        authorized -> paid
        proposed -> rejected
        pending_signoff -> rejected
    }

    on propose(id: String, amount: Int) {
        if amounts.get(id) != () { fail("conflict", "payment {id} already exists") }
        amounts.set(id, amount)                     // > 0 and <= 1,000,000 or it raises
        return payment(id)
    }
    on authorize(id: String) {
        let amount = _amount(id)
        if amount > 10000 {
            transition(id, "pending_signoff")      // large: a second person first
        } else {
            transition(id, "authorized")
        }
        return payment(id)
    }
    on sign_off(id: String, officer: String) {
        require officer != "" else OfficerRequired
        transition(id, "signed_off")
        transition(id, "authorized")
        return payment(id)
    }
    on settle(id: String) {
        // Either order is safe — the handler is atomic. Here the transition
        // goes first: when the ledger refuses the debit, the transition is
        // rolled back and the payment is still `authorized`.
        transition(id, "paid")
        _debit(_amount(id))
        return payment(id)
    }
    on reject(id: String) {
        transition(id, "rejected")
        return payment(id)
    }
    on payment(id: String) {
        return map("id", id, "amount", _amount(id), "state", get_status(id))
    }
    on _amount(id: String) {
        let a = amounts.get(id)
        if a == () { fail("not_found", "payment {id}") }
        return a
    }

    // Error kinds → HTTP statuses. Every route is spelled out here, so the
    // handlers above are reached through this router (soma check warns when a
    // route and a handler share a name; here they do not).
    on request(method: String, path: String, body: String) {
        let r = try { _route(method, path, body) }
        if r.error == () { return r.value }
        if r.kind == "not_found" { return response(404, map("error", r.detail)) }
        if r.kind == "conflict" { return response(409, map("error", r.detail)) }
        if r.kind == "invalid_transition" { return response(409, map("error", r.error)) }
        if r.kind == "invariant" { return response(422, map("error", "refused by the ledger", "detail", r.error)) }
        return response(422, map("error", r.error))
    }
    on _route(method: String, path: String, body: String) {
        return match map("method", method, "path", path) {
            {method: "POST", path: "/payments"} -> {
                let p = from_json(body)
                propose(p.id, p.amount)
            }
            {method: "POST", path: "/payments/authorize/" + id} -> authorize(id)
            {method: "POST", path: "/payments/settle/" + id} -> settle(id)
            {method: "POST", path: "/payments/reject/" + id} -> reject(id)
            {method: "GET", path: "/payments/" + id} -> payment(id)
            {method: "GET", path: "/balance"} -> map("balance", balance())
            _ -> response(404, map("error", "no route"))
        }
    }
}

cell test PaymentsTests {
    rules {
        assert _deposit(500) == 500
        // happy path, small payment
        assert propose("p1", 200).state == "proposed"
        assert authorize("p1").state == "authorized"
        assert settle("p1").state == "paid"
        assert balance() == 300
        // a rejected debit leaves the payment where it was — atomic handler
        assert propose("p2", 900).state == "proposed"
        assert authorize("p2").state == "authorized"
        let s = try { settle("p2") }
        assert s.kind == "invariant"
        assert payment("p2").state == "authorized"
        assert balance() == 300
        // large payments need a second person; the proof: paid requires authorized
        assert propose("p3", 50000).state == "proposed"
        assert authorize("p3").state == "pending_signoff"
        assert_fails settle("p3") matching "invalid transition"
        assert sign_off("p3", "cfo").state == "authorized"
        // errors carry kinds the router turns into statuses
        assert_fails propose("p1", 1) matching "conflict"
        assert_fails payment("nope") matching "not_found"
        assert_fails propose("p4", 0) matching "invariant"
        assert request("GET", "/payments/nope", "")._status == 404
        assert request("POST", "/payments/settle/p1", "")._status == 409
        assert request("GET", "/balance", "").balance == 300
        assert request("POST", "/payments", "{\"id\":\"p5\",\"amount\":10}").state == "proposed"
    }
}

What the toolchain says

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

$ soma check payments_approval.cell
✓ All checks passed.
$ soma test payments_approval.cell
test PaymentsTests ...
  ✓ assert _deposit(500) == 500
  ✓ assert propose("p1", 200).state == "proposed"
  ✓ assert authorize("p1").state == "authorized"
  ✓ assert settle("p1").state == "paid"
  ✓ assert balance() == 300
  ✓ assert propose("p2", 900).state == "proposed"
  ✓ assert authorize("p2").state == "authorized"
  ✓ assert s.kind == "invariant"
  ✓ assert payment("p2").state == "authorized"
  ✓ assert balance() == 300
  ✓ assert propose("p3", 50000).state == "proposed"
  ✓ assert authorize("p3").state == "pending_signoff"
  ✓ assert_fails settle("p3") matching "invalid transition" — raised invalid transition: pending_signoff → paid. Current state: 'pending_signoff'. Valid targets: [signed_off, rejected]
  ✓ assert sign_off("p3", "cfo").state == "authorized"
  ✓ assert_fails propose("p1", 1) matching "conflict" — raised conflict: payment p1 already exists
  ✓ assert_fails payment("nope") matching "not_found" — raised not_found: payment nope
  ✓ assert_fails propose("p4", 0) matching "invariant" — raised memory invariant violated on 'amounts': amounts > 0 && amounts <= 1000000 — rejected write of 0 (key "p4"); the slot is unchanged
  ✓ assert request("GET", "/payments/nope", "")._status == 404
  ✓ assert request("POST", "/payments/settle/p1", "")._status == 409
  ✓ assert request("GET", "/balance", "").balance == 300
  ✓ assert request("POST", "/payments", "{\"id\":\"p5\",\"amount\":10}").state == "proposed"

21 tests: 21 passed, 0 failed
$ soma verify payments_approval.cell
Verifying payments_approval.cell...

State machine 'flow': 6 states, initial 'proposed'
  States: [authorized, paid, pending_signoff, proposed, rejected, signed_off]

  ✓ 6 states, 7 transitions
  ✓ all states reachable from 'proposed'
  ✓ terminal states: [paid, rejected]
  ✓ no deadlocks
  ✓ liveness: every state can eventually reach a terminal state
  ✓ think-isolated: CTL safety properties hold regardless of LLM output (4 handlers, 6 literal transitions, 0 dynamic)
  ✓ termination: all 9 handlers structurally terminate
  ✓ refinement: handler `authorize` ⟶ {pending_signoff [if amount > 10000], authorized [if not (amount > 10000)]}
  ✓ refinement: handler `sign_off` ⟶ {signed_off, signed_off → authorized}
  ✓ refinement: handler `settle` ⟶ {paid}
  ✓ refinement: handler `reject` ⟶ {rejected}
  · transition sources: `authorize`, `sign_off`, `settle`, `reject` 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)

Ledger/invariants:

  ✓ invariant account >= 0 — writer '_deposit' proven by induction (writes balance() + amount)
  ⚠ invariant account >= 0 — runtime-checked (computed values): _debit → account

Payments/invariants:

  ⚠ invariant amounts > 0 && amounts <= 1000000 — runtime-checked (computed values): propose → amounts because `amount` is a parameter (narrow it: `require amount > 0 && amount <= 1000000 else …`)

12 passed, 2 warnings, 0 failures

  Temporal properties for 'flow':
  ✓ deadlock_free — no deadlocks in any reachable state
  ✓ eventually(state in [paid, rejected]) — all paths eventually satisfy the predicate
  ✓ requires('paid' only after one of [authorized]) — every path to 'paid' passes through one of [authorized]
  ✓ after('rejected', never 'paid') — 'paid' is unreachable once 'rejected' has been reached
  ✓ after('rejected', never 'authorized') — 'authorized' is unreachable once 'rejected' has been reached
  ✓ after('rejected', never 'signed_off') — 'signed_off' is unreachable once 'rejected' has been reached
  ✓ after('paid', never 'rejected') — 'rejected' is unreachable once 'paid' has been reached
soma.toml: 6 user-defined properties loaded
Temporal: 7 passed, 0 failed
note: state machines are verified per cell. A value crosses a cell boundary by a face contract (`requires` / `ensures` on the signal); a rule between two cells' machines is proven when soma.toml states it — `[verify.before."Pay.paid"] requires = ["Approval.signed_off"]` explores the product of the machines.
VERIFY OK
$ soma run payments_approval.cell request POST /payments '{"id":"p1","amount":250}'
{"id": "p1", "amount": 250, "state": "proposed"}

Builtins used

fail, from_json, get_status, map, response, transition

Source file: /corpus/services/payments_approval.cell · examples/corpus/services/payments_approval.cell