Document editorial workflow with a typed (sum-type) state machine
A draft is submitted for review; a reviewer either requests changes (back to draft) or approves; approved documents are published. The changes-requested loop is bounded by reaching `published` or `archived` from every state, so liveness holds.
A Soma program from the state_machines part of the verified corpus: 59 lines, cells DocState, Editorial, EditorialTests. It passes soma check with soma 3.2.4.
How it is built
The cell Editorial declares 7 signals in its face: create(id: String) -> String, submit(id: String) -> String, request_changes(id: String) -> String, approve_doc(id: String) -> String, publish_doc(id: String) -> String, archive(id: String) -> String and status(id: String) -> String. Its state machine doc starts in Draft and allows 5 transitions: Draft -> InReview, InReview -> Draft, InReview -> Approved, Approved -> Published and * -> Archived. Archived and Published are terminal. Handlers: create (line 31), submit (line 32), request_changes (line 33), approve_doc (line 34), publish_doc (line 35), archive (line 36) and status (line 37).
The file also holds 1 test cell (EditorialTests) with 9 assertions, run by soma test.
The source
document_review.cell
// Document editorial workflow with a typed (sum-type) state machine.
// A draft is submitted for review; a reviewer either requests changes
// (back to draft) or approves; approved documents are published. The
// changes-requested loop is bounded by reaching `published` or `archived`
// from every state, so liveness holds.
cell type DocState {
variants { Draft InReview Approved Published Archived }
}
cell Editorial {
face {
signal create(id: String) -> String
signal submit(id: String) -> String
signal request_changes(id: String) -> String
signal approve_doc(id: String) -> String
signal publish_doc(id: String) -> String
signal archive(id: String) -> String
signal status(id: String) -> String
}
state doc: DocState {
initial: Draft
Draft -> InReview
InReview -> Draft
InReview -> Approved
Approved -> Published
* -> Archived
}
on create(id: String) { return get_status(id) }
on submit(id: String) { transition(id, InReview) return get_status(id) }
on request_changes(id: String) { transition(id, Draft) return get_status(id) }
on approve_doc(id: String) { transition(id, Approved) return get_status(id) }
on publish_doc(id: String) { transition(id, Published) return get_status(id) }
on archive(id: String) { transition(id, Archived) return get_status(id) }
on status(id: String) { return get_status(id) }
}
cell test EditorialTests {
rules {
// submit -> request changes -> resubmit -> approve -> publish
assert create("d1") == "Draft"
assert submit("d1") == "InReview"
assert request_changes("d1") == "Draft"
assert submit("d1") == "InReview"
assert approve_doc("d1") == "Approved"
assert publish_doc("d1") == "Published"
// any document can be archived (the * arrow), here from Draft
assert archive("d2") == "Archived"
// illegal: cannot publish straight from Draft
assert_fails publish_doc("d3")
// illegal: cannot approve a document that isn't in review
assert_fails approve_doc("d4")
}
}What the toolchain says
$ soma check document_review.cell
✓ All checks passed.$ soma test document_review.cell
test EditorialTests ...
✓ assert create("d1") == "Draft"
✓ assert submit("d1") == "InReview"
✓ assert request_changes("d1") == "Draft"
✓ assert submit("d1") == "InReview"
✓ assert approve_doc("d1") == "Approved"
✓ assert publish_doc("d1") == "Published"
✓ assert archive("d2") == "Archived"
✓ assert_fails publish_doc("d3") — raised invalid transition: Draft → Published. Current state: 'Draft'. Valid targets: [InReview, Archived]
✓ assert_fails approve_doc("d4") — raised invalid transition: Draft → Approved. Current state: 'Draft'. Valid targets: [InReview, Archived]
9 tests: 9 passed, 0 failed$ soma verify document_review.cell
Verifying document_review.cell...
State machine 'doc': 5 states, initial 'Draft'
States: [Approved, Archived, Draft, InReview, Published]
✓ 5 states, 8 transitions
✓ all states reachable from 'Draft'
✓ terminal states: [Archived]
✓ no deadlocks
✓ liveness: every state can eventually reach a terminal state
✓ wildcard transitions: * -> [Archived]
⚠ `* -> Archived` also adds Published -> Archived — so Published is not final. If it should stay final, write `* -> Archived except [Published]`
✓ think-isolated: CTL safety properties hold regardless of LLM output (5 handlers, 5 literal transitions, 0 dynamic)
✓ termination: all 7 handlers structurally terminate
✓ refinement: handler `submit` ⟶ {InReview}
✓ refinement: handler `request_changes` ⟶ {Draft}
✓ refinement: handler `approve_doc` ⟶ {Approved}
✓ refinement: handler `publish_doc` ⟶ {Published}
✓ refinement: handler `archive` ⟶ {Archived}
· transition sources: `submit`, `request_changes`, `approve_doc`, `publish_doc`, `archive` 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)
13 passed, 1 warnings, 0 failures
Temporal properties for 'doc':
✓ deadlock_free — no deadlocks in any reachable state
Temporal: 1 passed, 0 failed
VERIFY OK