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