soma.cell

Three readings — an ordinance cannot be law the night it is introduced

Most municipal charters require an ordinance to be read at three separate council meetings before adoption. The rule is anti-ambush: the public gets two full meeting cycles to notice, organize, and object before anything binds them. The standard evasion is the "emergency clause" — Ohio councils became notorious for passing garden-variety zoning as an "emergency" to skip readings and, not coincidentally, to dodge the citizen referendum that attaches to normal ordinances. In this machine the readings are not a custom the clerk remembers — they are the only path. `adopted` is reachable solely through three reading states, soma verify proves no shortcut edge exists, and each reading can be logged at most once per ordinance stage: stuffing all three readings into one wild night is a write the journal refuses.

A Soma program from the governance part of the verified corpus: 145 lines, cells CouncilJournal, ReadingsTests. It passes soma check with soma 3.2.4.

How it is built

The cell CouncilJournal declares 5 signals in its face: introduce(ord: String) -> Map, read_aloud(ord: String, meeting_no: Int) -> Map, adopt_ordinance(ord: String) -> Map, withdraw(ord: String) -> Map and readings_of(ord: String) -> Int; keeps 2 memory slots: journal (Map<String, Int>, persistent and consistent) under the invariant journal >= 1 and logged (Map<String, Int>, persistent and consistent) under the invariant logged >= 0 && logged <= 1. Its state machine ordinance starts in introduced and allows 5 transitions: introduced -> first_reading, first_reading -> second_reading, second_reading -> third_reading, third_reading -> adopted and introduced -> withdrawn. adopted and withdrawn are terminal. Handlers: introduce (line 52), read_aloud (line 56), adopt_ordinance (line 91), withdraw (line 97) and readings_of (line 103).

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

Generated from soma describe on this file.

The source

three_readings.cell

// ============================================================================
// THREE READINGS — an ordinance cannot be law the night it is introduced
// ============================================================================
//
// Most municipal charters require an ordinance to be read at three
// separate council meetings before adoption. The rule is anti-ambush:
// the public gets two full meeting cycles to notice, organize, and
// object before anything binds them. The standard evasion is the
// "emergency clause" — Ohio councils became notorious for passing
// garden-variety zoning as an "emergency" to skip readings and, not
// coincidentally, to dodge the citizen referendum that attaches to
// normal ordinances.
//
// In this machine the readings are not a custom the clerk remembers —
// they are the only path. `adopted` is reachable solely through three
// reading states, soma verify proves no shortcut edge exists, and each
// reading can be logged at most once per ordinance stage: stuffing all
// three readings into one wild night is a write the journal refuses.
// ============================================================================

cell CouncilJournal {
    face {
        signal introduce(ord: String) -> Map
        signal read_aloud(ord: String, meeting_no: Int) -> Map
        signal adopt_ordinance(ord: String) -> Map
        signal withdraw(ord: String) -> Map
        signal readings_of(ord: String) -> Int
    }

    memory {
        // ord:reading_number -> the meeting it happened at. Each of the
        // three readings is logged once; a duplicate log entry for the
        // same reading is unrepresentable.
        journal: Map<String, Int> [persistent, consistent]
        invariant journal >= 1

        logged: Map<String, Int> [persistent, consistent]
        invariant logged >= 0 && logged <= 1
    }

    // introduced -> r1 -> r2 -> r3 -> adopted (TERMINAL).
    // withdrawn is the other terminal exit. NO edge skips a reading.
    state ordinance {
        initial: introduced
        introduced -> first_reading
        first_reading -> second_reading
        second_reading -> third_reading
        third_reading -> adopted
        introduced -> withdrawn
    }

    on introduce(ord: String) {
        return map("ok", true, "ord", ord, "state", get_status(ord))
    }

    on read_aloud(ord: String, meeting_no: Int) {
        let s = get_status(ord)
        let target = ""
        let n = 0
        if s == "introduced" {
            target = "first_reading"
            n = 1
        }
        if s == "first_reading" {
            target = "second_reading"
            n = 2
        }
        if s == "second_reading" {
            target = "third_reading"
            n = 3
        }
        if target == "" {
            return map("ok", false, "reason", "no reading due from state {s}")
        }
        // same meeting cannot host two readings of the same ordinance:
        // the previous reading's meeting number must differ
        if n > 1 {
            let prev = journal.get("{ord}:r{n - 1}")
            if prev == meeting_no {
                return map("ok", false, "reason",
                    "reading {n} must happen at a SEPARATE meeting (got {meeting_no} again)")
            }
        }
        let w = try { logged.set("{ord}:r{n}", (logged.get("{ord}:r{n}") ?? 0) + 1) }
        if w.error != () { return map("ok", false, "reason", "reading {n} already logged") }
        journal.set("{ord}:r{n}", meeting_no)
        transition(ord, target)
        return map("ok", true, "reading", n, "meeting", meeting_no)
    }

    on adopt_ordinance(ord: String) {
        let r = try { transition(ord, "adopted") }
        if r.error != () { return map("ok", false, "reason", "{r.error}") }
        return map("ok", true, "state", "adopted", "readings", readings_of(ord))
    }

    on withdraw(ord: String) {
        let r = try { transition(ord, "withdrawn") }
        if r.error != () { return map("ok", false, "reason", "{r.error}") }
        return map("ok", true, "state", "withdrawn")
    }

    on readings_of(ord: String) {
        let n = 0
        for k in logged.keys() {
            if starts_with(k, "{ord}:") { n = n + (logged.get(k) ?? 0) }
        }
        return n
    }
}

cell test ReadingsTests {
    rules {
        // THE EMERGENCY-CLAUSE MOVE: introduced tonight, law tonight. No edge.
        assert introduce("zoning-44").ok == true
        assert adopt_ordinance("zoning-44").ok == false
        assert_fails transition("zoning-44", "adopted")
        assert_fails transition("zoning-44", "second_reading")

        // reading one, meeting 101
        assert read_aloud("zoning-44", 101).reading == 1
        // the same night cannot host reading two
        assert read_aloud("zoning-44", 101).ok == false
        assert adopt_ordinance("zoning-44").ok == false

        // two more meeting cycles; the public has had its window
        assert read_aloud("zoning-44", 102).reading == 2
        assert read_aloud("zoning-44", 103).reading == 3
        assert readings_of("zoning-44") == 3
        assert adopt_ordinance("zoning-44").ok == true

        // adopted is terminal; the journal cannot double-log a reading
        assert_fails transition("zoning-44", "third_reading")
        assert_fails transition("zoning-44", "introduced")
        assert_fails logged.set("zoning-44:r2", 2)
        assert read_aloud("zoning-44", 104).ok == false

        // withdrawal is terminal too — a dead ordinance stays dead
        assert introduce("tax-9").ok == true
        assert withdraw("tax-9").ok == true
        assert_fails transition("tax-9", "first_reading")
        assert read_aloud("tax-9", 105).ok == false
    }
}

What the toolchain says

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

$ soma check three_readings.cell
✓ All checks passed.
$ soma test three_readings.cell
test ReadingsTests ...
  ✓ assert introduce("zoning-44").ok == true
  ✓ assert adopt_ordinance("zoning-44").ok == false
  ✓ assert_fails transition("zoning-44", "adopted") — raised invalid transition: introduced → adopted. Current state: 'introduced'. Valid targets: [first_reading, withdrawn]
  ✓ assert_fails transition("zoning-44", "second_reading") — raised invalid transition: introduced → second_reading. Current state: 'introduced'. Valid targets: [first_reading, withdrawn]
  ✓ assert read_aloud("zoning-44", 101).reading == 1
  ✓ assert read_aloud("zoning-44", 101).ok == false
  ✓ assert adopt_ordinance("zoning-44").ok == false
  ✓ assert read_aloud("zoning-44", 102).reading == 2
  ✓ assert read_aloud("zoning-44", 103).reading == 3
  ✓ assert readings_of("zoning-44") == 3
  ✓ assert adopt_ordinance("zoning-44").ok == true
  ✓ assert_fails transition("zoning-44", "third_reading") — raised invalid transition: adopted → third_reading. Current state: 'adopted'. Valid targets: []
  ✓ assert_fails transition("zoning-44", "introduced") — raised invalid transition: adopted → introduced. Current state: 'adopted'. Valid targets: []
  ✓ assert_fails logged.set("zoning-44:r2", 2) — raised memory invariant violated on 'logged': logged >= 0 && logged <= 1 — rejected write of 2 (key "zoning-44:r2"); the slot is unchanged
  ✓ assert read_aloud("zoning-44", 104).ok == false
  ✓ assert introduce("tax-9").ok == true
  ✓ assert withdraw("tax-9").ok == true
  ✓ assert_fails transition("tax-9", "first_reading") — raised invalid transition: withdrawn → first_reading. Current state: 'withdrawn'. Valid targets: []
  ✓ assert read_aloud("tax-9", 105).ok == false

19 tests: 19 passed, 0 failed
$ soma verify three_readings.cell
Verifying three_readings.cell...

State machine 'ordinance': 6 states, initial 'introduced'
  States: [adopted, first_reading, introduced, second_reading, third_reading, withdrawn]

  ✓ 6 states, 5 transitions
  ✓ all states reachable from 'introduced'
  ✓ terminal states: [adopted, withdrawn]
  ✓ no deadlocks
  ✓ liveness: every state can eventually reach a terminal state
  ⚠ dynamic transition targets: handler `read_aloud` uses a dynamic (computed) transition target — the state these handlers move to is computed at run time (from input or an LLM answer), so the machine's safety is checked at run time for them, not proven
  ✓ termination: all 5 handlers structurally terminate
  ⚠ refinement (partial — dynamic targets fall back to runtime check): handler `read_aloud` calls transition() with a computed target — which edge it takes is checked at run time (invalid_transition), not proven; write the target as a literal state to have it verified
  ✓ refinement (partial — dynamic targets fall back to runtime check): handler `read_aloud` ⟶ <dynamic>
  ✓ refinement (partial — dynamic targets fall back to runtime check): handler `adopt_ordinance` ⟶ {adopted}
  ✓ refinement (partial — dynamic targets fall back to runtime check): handler `withdraw` ⟶ {withdrawn}
  · transition sources: `adopt_ordinance`, `withdraw` 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)

CouncilJournal/invariants:

  ⚠ invariant journal >= 1 — runtime-checked (computed values): read_aloud → journal because `meeting_no` is a parameter (narrow it: `require meeting_no >= 1 else …`)
  ✓ invariant logged >= 0 — writer 'read_aloud' proven by induction (writes (logged.get("{ord}:r{n}") ?? 0) + 1)
  ✓ invariant logged >= 0 — writer 'read_aloud' proven by induction (writes (logged.get("{ord}:r{n}") ?? 0) + 1)
  ⚠ invariant logged >= 0 && logged <= 1 — runtime-checked (computed values): read_aloud → logged [logged <= 1] because `logged` has no known range

11 passed, 4 warnings, 0 failures

  Temporal properties for 'ordinance':
  ✓ deadlock_free — no deadlocks in any reachable state
Temporal: 1 passed, 0 failed
VERIFY OK

Builtins used

get_status, map, starts_with, transition

Source file: /corpus/governance/three_readings.cell · examples/corpus/governance/three_readings.cell