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

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.

Generated from soma describe on this file.

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

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

$ 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

Builtins used

get_status, transition

Source file: /corpus/state_machines/document_review.cell · examples/corpus/state_machines/document_review.cell