Search/Build and Change Event-Driven State Machines Without Breaking Their Behavior

Build and Change Event-Driven State Machines Without Breaking Their Behavior

Use when building or changing a state-machine library. The AI finishes each event before the next, handles errors without skipping cleanup, keeps threads and child machines safe, writes tests first for sync and async engines, and keeps code, docs, and schemas in sync.

To use this AI rule
## Decide Which Module Owns Each Piece of State-Machine Logic Map responsibilities before changing code so behavior stays in the component that owns the required knowledge. ### Define clear boundaries - Separate state and event definitions, transition composition, callback dispatch, signature adaptation, execution engines, registries, integrations, and serialization. - Keep general infrastructure such as dispatch and signature adaptation independent of the state-machine domain. - Give each module, class, and function one clear reason to change. - Prefer small, focused modules with explicit interfaces over extending an unrelated module for convenience. ### Place logic deliberately - Apply Information Expert: compute a result where the necessary knowledge already exists, then return or signal it instead of recomputing it elsewhere. - Apply the Law of Demeter: pass the specific value a collaborator needs rather than its parent object. - Apply Interface Segregation: accept a narrow field or protocol when a helper does not need the full object. - Preserve public behavior when refactoring, and identify the tests that characterize it before moving logic. ## Finish Processing One Event Before Taking the Next Model event processing at two distinct levels and do not collapse them during implementation. ### Distinguish processing levels - A microstep atomically executes one selected transition set in the defined order: before callbacks, exit callbacks, transition actions, entry callbacks, and after callbacks. - A macrostep handles one external event and repeats microsteps until the machine reaches a stable configuration. - A configuration is stable only when no eventless transition is enabled and the internal queue is empty. ### Keep queue and event semantics explicit - Events sent externally enter the external queue and wait until the current macrostep finishes. - Events raised internally enter the internal queue and run within the current macrostep before later external events. - An unassigned transition declaration may represent an eventless transition that fires when its guard becomes true. - An assigned transition creates a named event; if aliases are derived from naming conventions, an explicit identifier takes precedence. - Test queue ordering, eventless progression, and stable-configuration termination as separate behaviors. ## Turn Errors into Events Without Skipping Cleanup Callbacks Define where errors are intercepted and which lifecycle callbacks still run after a failure. ### Set the error boundary - Make the catch-errors-as-events policy explicit for each machine type; chart-oriented and basic variants may intentionally use opposite defaults. - Catch errors at the action-block level rather than treating an entire microstep as one catch boundary, and still run finalization callbacks on both success and failure when the lifecycle guarantees them. - Dispatch a caught execution error as an internal event so the statechart can handle it through declared transitions. - Prevent recursive failure loops by suppressing or terminating errors raised while handling the internal execution-error event. ### Preserve callback distinctions - Separate generic lifecycle callbacks from event-specific and state-specific callbacks. - An error-named callback is not automatically generic; require the corresponding error transition before invoking it by convention. - If transition actions disable local error interception during error-event processing, keep that rule distinct from entry and exit blocks that always use their error handler. - Test success, ordinary failure, failure inside error handling, and finalization order independently. ## Keep Threads and Child Machines Safe When Changing Queues or Locks Keep thread-safety and invocation-routing guarantees intact when changing queues, locks, or child-machine management. ### Maintain safe concurrent processing - Allow at most one thread to execute transitions for a machine instance at a time. - Use a thread-safe priority queue when concurrent senders are supported. - Do not replace a thread-safe queue with a plain list or non-thread-safe deque without redesigning and proving the concurrency contract. - Stress-test real contention with coordinated sender threads and barriers. - Use interruptible event waits with explicit timeouts in blocking-thread tests instead of fixed sleeps that can leak threads during teardown. ### Manage invoked children precisely - Remove a child invocation only when it is both terminated and cancelled. - Keep a terminated but not cancelled child routable while its owning state remains active. - If child construction blocks inside the processing loop, capture the child reference through an early lifecycle listener before the loop continues. - Route child-targeted events through the invocation manager and the child handler rather than bypassing lifecycle control. - Test cancellation, natural termination, retained routing, and late event delivery as distinct cases. ## Run Tests in the Project's Set Environment and Measure Branch Coverage Run installation, tests, quality checks, and documentation through the project’s pinned environment and task runner. ### Execute tests safely - Put an explicit timeout around the full suite to expose leaked threads or infinite loops. - Use supported parallel execution for the full suite when tests contain intentional waiting or fallback timeouts. - Invoke the canonical suite from the project root when path-restricted invocation would omit module or documentation doctests. - Investigate runs that exceed the established baseline instead of repeatedly rerunning them without diagnosis. - Enable verbose debug logging only for targeted investigations when global debug logging changes performance measurements. - Capture extensive output once, then analyze the saved result instead of repeatedly running the same expensive suite. ### Measure the intended coverage - Activate coverage collection explicitly when required, then generate a missing-line or machine-readable report for affected modules; selecting a report format alone may not enable collection. - Verify branch coverage, not only line coverage, before committing. ## Write a Failing Test First and Run It on Sync and Async Engines Treat tests as implementation requirements rather than cleanup performed after the code is written. ### Follow a test-driven loop 1. Identify new branches, edge cases, error paths, and concurrency risks while planning. 2. Add a focused test that fails for the missing or incorrect behavior. 3. Implement the smallest behavior change that makes the test pass. 4. Run the targeted test, affected-module coverage, and the required broader checks. 5. Refactor only after behavior and coverage are protected. ### Reuse test infrastructure - Use a shared parametrized runner to exercise the same statechart against synchronous and asynchronous engines. - Let the runner abstract engine startup and event delivery instead of duplicating test classes or adding no-op listeners. - Use fixtures for temporary paths, environment changes, and mutable state; do not hardcode paths or rely on mutable globals. - Cover defensive branches with tests whenever they are reachable; exclude a branch from coverage only when its impossibility is established. - Keep the required branch-coverage threshold enforced by the standard commit checks. ## Run Code Checks and Keep the Parser and Schema in Sync Apply one consistent quality system to implementation, examples, generated contracts, and developer documentation. ### Enforce code conventions - Run the project’s formatter, linter, import sorter, type checker, and tests through the canonical toolchain. - Prefer sorted top-level imports; use lazy imports only when they are necessary to break a circular dependency. - Follow the project’s naming and docstring conventions, and document hidden semantics rather than repeating obvious names or types. - Use an attribute docstring directly below the attribute when required by the documentation generator; reserve inline comments for non-obvious logic. ### Test documentation and declarative formats - Make documentation examples executable doctests when practical. - Put examples that require real concurrency in unit tests and reference them from the documentation. - Keep the parser, runtime behavior, and published schema synchronized whenever the declarative vocabulary changes. - Treat the parser’s accepted-key sets as the source of truth and test them against the matching schema properties. - Validate every structured example and fixture against the schema so parser-schema drift fails the test suite. ## Finish Changes Without Skipping Commit Hooks or Committing Secrets Use the repository’s contribution process to preserve traceability, generated artifacts, and security boundaries. ### Complete the quality gate - Target the repository’s designated development branch and follow its commit-message convention. - Use signed commits when the project prefers them. - Never bypass commit hooks or disable required checks. - If a hook regenerates or modifies files, review and stage those results, then commit again until every hook passes. - Review the final diff and confirm that implementation, tests, documentation, and generated contracts agree. ### Protect trust boundaries - Never commit secrets, credentials, or environment files. - Validate untrusted data at system boundaries and keep internal invariants explicit. - Do not weaken queue safety, error-loop prevention, schema validation, or coverage enforcement merely to make a change pass. - Report any required check that could not run and do not claim completion without direct results.