--- # Authoritative formal design for realtime machine M5: TTS pipeline lifecycle. # # Companion to: # - docs/design/realtime-state-machines.md (the map + invariants) # - core/http/endpoints/openai/ttscoord (the Go implementation) # # The Go MBT adapter maps each action below onto ttscoord.Coordinator.Apply and # the StateGetter onto ttscoord.Coordinator.State, so this spec is the source of # truth the implementation is checked against. # # The TTS pipeline's open->closing->closed lifecycle (the legacy `closed` bool + # `done` channel). Two writers: the producer raises Close (wait()), the worker # raises WorkerExited. `phase` is 0=open, 1=closing, 2=closed; `wakes` counts how # many times Close woke the worker to exit. # # NOTE: FizzBee is pre-1.0. Validate the exact syntax/CLI against the version # pinned in formal-verification/README.md before trusting the gate. deadlock_detection: false --- role Pipeline: action Init: self.phase = 0 # 0 open, 1 closing, 2 closed -- monotonic self.wakes = 0 # Close->Closing transitions (worker wakeups to exit) # wait() called (producer). THE FIX: it advances to closing and wakes the # worker only from open, so wait() is idempotent. To reproduce the legacy # double-wake hazard, drop the `if self.phase == 0` guard (always wake): the # checker then reports WakeOnce violated. atomic action Close: if self.phase == 0: self.phase = 1 self.wakes += 1 # The worker drained the queue and observed the close (worker goroutine). atomic action WorkerExited: if self.phase == 1: self.phase = 2 action Init: p = Pipeline() # SAFETY: the worker is woken-to-exit at most once -- the done channel is joined # exactly once, wait() is idempotent (Part 4, invariant #10). always assertion WakeOnce: return p.wakes <= 1 # SAFETY: the lifecycle is bounded and monotonic open -> closing -> closed; a # clause is never accepted after close (enqueue is gated on phase 0 in Go) and # the worker is joined exactly once (Part 4, invariant #8). always assertion Monotonic: return p.phase >= 0 and p.phase <= 2