This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
./gradlew build # build all modules
./gradlew :tests:jvmTest # run all tests (JVM target)
./gradlew :tests:jvmTest --tests "ru.nsk.kstatemachine.statemachine.StateMachineTest" # run a single test class
./gradlew apiDump # regenerate .api files after public API changes
./gradlew apiCheck # validate public API matches committed .api files (runs in build)
./gradlew koverReport # generate code coverage report (min 88%)Five Gradle subprojects:
kstatemachine— core library. Zero dependencies: no Kotlin Coroutines, no Android SDK. Uses stdlib coroutine primitives only viaStdLibCoroutineAbstraction. Entry point:createStdLibStateMachine { }.kstatemachine-coroutines— adds full Kotlin Coroutines support. Entry point:createStateMachine(scope) { }(suspend). ProvidesprocessEventByLaunch/processEventByAsyncandStateMachineFlowforFlow-based observation.kstatemachine-serialization— addskotlinx.serializationsupport for persistingRecordedEventsviaRecordedEventsSerializer.tests— all integration/unit tests. Depends on all three library modules. Uses Kotest + MockK. Only JVM target is run locally (other targets require platform toolchains).samples— standalone usage examples, excluded from API validation.
All library versions live in buildSrc/src/main/kotlin/Versions.kt.
CoroutineAbstraction (in kstatemachine) is the key seam that lets the core library expose suspend functions without depending on kotlinx.coroutines. StdLibCoroutineAbstraction drives coroutines via raw Continuation from the stdlib; CoroutinesLibCoroutineAbstraction (in kstatemachine-coroutines) wraps a CoroutineScope.
Important thread-safety constraint: StateMachine is not thread-safe. Always use a single-threaded CoroutineScope. The library enforces this at construction time and rejects Dispatchers.Default / Dispatchers.IO unless skipCoroutineScopeValidityCheck is set.
IState ──► State (plain state, no data)
──► DataState<D> (holds typed data while active)
──► MutableDataState<D> (data settable manually)
──► IFinalState (machine stops on entry)
──► PseudoState (machine passes through automatically)
──► HistoryState / RedirectPseudoState
StateMachine extends State (a machine is itself a state → composable)
ChildMode.EXCLUSIVE (default) = one active child at a time; ChildMode.PARALLEL = all children active simultaneously.
Events are processed synchronously within a single-threaded context. While an event is being processed, additional events are queued and dispatched to PendingEventHandler. processEvent() returns ProcessingResult (PROCESSED / IGNORED / PENDING).
CoVisitor (suspendable) and Visitor (sync) walk the machine tree: StateMachine → IState → Transition. Used internally for export (PlantUML/Mermaid), structure hashing, active-state collection, and cleanup. Prefer adding new cross-cutting traversals as visitors rather than recursive extension functions.
EventRecorder captures every processed event into RecordedEvents (enabled via CreationArguments.eventRecordingArguments). To restore state: replay RecordedEvents on a freshly constructed identical machine via restoreByRecordedEvents(). The structureHashCode guards against replaying on a structurally different machine. kstatemachine-serialization provides a RecordedEventsSerializer for JSON persistence.
The binary-compatibility-validator plugin tracks public API in <module>/api/<module>.api files. After any public API change run ./gradlew apiDump to update these files and commit them alongside the code change. apiCheck runs as part of build and will fail if the files are out of date.
Testing object (imported via import ru.nsk.kstatemachine.testing.Testing.*) provides startFrom(state) / startFromBlocking(state) to start the machine from a specific state, bypassing normal initialization. The @VisibleForTesting annotation marks internal API exposed only for tests; such members are excluded from public API validation.