subroutine — state-machine discipline
Apply this to lifecycle transitions. The nearest AGENTS.md and event contracts
define the vocabulary.
Model states and events, not boolean combinations
- Give each state variant only valid fields; avoid booleans plus optionals.
- Model events separately and match with
.exhaustive() so additions fail
compilation until handled.
- Return a typed error for an illegal transition; never throw it or silently
retain the old state.
type JobState =
| { status: "idle" }
| { status: "running"; runId: string; startedAt: Date }
| { status: "failed"; runId: string; reason: string };
type JobEvent =
| { type: "START"; runId: string; at: Date }
| { type: "FAIL"; runId: string; reason: string };
function transition(state: JobState, event: JobEvent): Result<JobState, TransitionError> {
return match(event)
.with({ type: "START" }, ({ runId, at }) =>
state.status === "idle"
? ok<JobState>({ status: "running", runId, startedAt: at })
: err<TransitionError>({ code: "INVALID_TRANSITION", from: state.status, event: "START" }),
)
.with({ type: "FAIL" }, ({ runId, reason }) =>
state.status === "running" && state.runId === runId
? ok<JobState>({ status: "failed", runId, reason })
: err<TransitionError>({ code: "INVALID_TRANSITION", from: state.status, event: "FAIL" }),
)
.exhaustive();
}
Keep transitions deterministic
- Keep transitions pure: no I/O, clock, UUID, logging, or mutation. Pass generated
values as explicit inputs and execute effects afterward.
- Define duplicate, stale, and out-of-order behavior. Use stable event IDs or
aggregate versions when delivery repeats.
- Enforce writes with a transaction or optimistic version at persistence, never
an in-memory guard.
Test valid/invalid transitions and duplicate/stale behavior. Assert the next
state; test separate effect plans at their boundary.
1---2name: state-machine3description: State-machine discipline for TypeScript lifecycle, workflow, reducer, and state-machine files — make illegal states unrepresentable, keep transitions pure and exhaustive, and design for replay and concurrency.4---56# subroutine — state-machine discipline78Apply this to lifecycle transitions. The nearest `AGENTS.md` and event contracts9define the vocabulary.1011## Model states and events, not boolean combinations1213- Give each state variant only valid fields; avoid booleans plus optionals.14- Model events separately and match with `.exhaustive()` so additions fail15 compilation until handled.16- Return a typed error for an illegal transition; never throw it or silently17 retain the old state.1819```ts20type JobState =21 | { status: "idle" }22 | { status: "running"; runId: string; startedAt: Date }23 | { status: "failed"; runId: string; reason: string };2425type JobEvent =26 | { type: "START"; runId: string; at: Date }27 | { type: "FAIL"; runId: string; reason: string };2829function transition(state: JobState, event: JobEvent): Result<JobState, TransitionError> {30 return match(event)31 .with({ type: "START" }, ({ runId, at }) =>32 state.status === "idle"33 ? ok<JobState>({ status: "running", runId, startedAt: at })34 : err<TransitionError>({ code: "INVALID_TRANSITION", from: state.status, event: "START" }),35 )36 .with({ type: "FAIL" }, ({ runId, reason }) =>37 state.status === "running" && state.runId === runId38 ? ok<JobState>({ status: "failed", runId, reason })39 : err<TransitionError>({ code: "INVALID_TRANSITION", from: state.status, event: "FAIL" }),40 )41 .exhaustive();42}43```4445## Keep transitions deterministic4647- Keep transitions pure: no I/O, clock, UUID, logging, or mutation. Pass generated48 values as explicit inputs and execute effects afterward.49- Define duplicate, stale, and out-of-order behavior. Use stable event IDs or50 aggregate versions when delivery repeats.51- Enforce writes with a transaction or optimistic version at persistence, never52 an in-memory guard.5354Test valid/invalid transitions and duplicate/stale behavior. Assert the next55state; test separate effect plans at their boundary.