v3.0.0 — execution engine rewrite, typed errors, hardened builder - #17
Conversation
- States, transitions, and Process are now constructed only via the builder (internal symbol-based construction key) - ProcessBuilder validates: missing/multiple initial state, unknown source, unknown target, empty event name, conflicting duplicate transitions, optional strict orphan-state detection - Process is frozen; State and Transition have no public mutators - Closes IMPLEMENTATION_REVIEW.md #3 (graph immutability), #5 (empty event names at build time), #6 (duplicate-with-conflict at build time)
The two-pass simplification of build() created Transitions pointing at Pass-1 stub States while Process exposed Pass-2 final States, so t.getTargetState() !== process.getState(t.getTargetState().getName()) even though the names matched. Statemachine traversal would land on orphan State instances at runtime. Replaced the multi-pass approach with a depth-first leaf-first construction that builds each target State before the Transition referencing it is created, guaranteeing referential identity for acyclic graphs. Cyclic graphs are handled by a second-sweep rebuild scoped only to the cycle members. Added a referential-identity test to lock the contract.
The DFS+sweep approach left transitions in cycle members pointing at orphan stub State instances rather than the final States exposed by Process. Cycle detection missed it because both source and target had non-empty transitions — they just pointed at the wrong objects. Replaced with a two-phase construction: build all final States empty, then create Transitions targeting those States and attach them via a new internal-only State._initTransitions method. Identity holds by construction for any graph topology (acyclic, cyclic, self-loop). Added regression tests for cycles and self-loops. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Per-instance FIFO operation queue serializes concurrent triggerEvent and checkTransitions calls on the same Statemachine instance - Two-phase observer lifecycle: BeforeTransitionObserver may veto, then Event-bound dispatcher commands run, then commit, then AfterTransitionObservers run with frozen TransitionFrame - Errors from after-observers no longer roll back state; one error rethrown directly, multiple aggregated into AggregateError - Constructor takes options object; getSelectedTransition / getCurrentContext removed from public surface - Mutex acquireLock no longer short-circuits on isAcquired() - Closes IMPLEMENTATION_REVIEW.md #1 (concurrent ops), #4 (transaction semantics)
…backObserver to v3 - TransitionLogger: reads from TransitionFrame; subject identity injection deferred to subclasses - StatefulStatusChanger: takes the subject at construction - CallbackObserver: documented as Event-only in v3
- Factory: uses v3 options-object Statemachine constructor; exposes attachBeforeObserver and attachAfterObserver - Removed SetupHelper and StateCollectionMerger; both depended on user-mutable State/Transition which no longer exist in v3 - Migration paths documented in docs/migration/v2-to-v3.md (Phase 7) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Note: timing workaround (setTimeout(0)) used in OnEnterObserver tests because triggerEvent's Promise currently resolves before chained ops drain — this is a known src/Statemachine.ts bug to fix separately. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Also fix CallbackCondition constructor arg order in process-builder.test.ts (signature is new CallbackCondition(name, callback), not callback-first). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…and #4 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Document chained-event timing explicitly in v2-to-v3 migration guide (await triggerEvent resolves before OnEnterObserver-enqueued chains drain; shows the setTimeout(0) yield pattern used in tests) - Remove dead if-block in TransitionLogger that contained only a comment Co-Authored-By: Claude Code <noreply@anthropic.com>
TypeScript 5.7 -> 6.0 (matches the typescript@^6.0.2 devDep). Node.js 18/20/22 -> 20/22/24 (matches the new engines.node range and CI matrix). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the gap between prepublishOnly (lint+test) and the CI pack job (build+pack): now an actual npm publish runs the full sequence — build, lint, test, and tarball verification — so a stale or missing dist cannot reach the registry through any path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ation The previous example claimed an event-based transition outranks an automatic transition during triggerEvent. In v3, automatic transitions are not in the active set when an event is supplied, so the comparison could not occur. Rewrite to compare event+condition vs event-only — two transitions that genuinely appear together — and add a subsection documenting the event vs automatic phase separation. Closes IMPLEMENTATION_REVIEW.md #9. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…r example The previous example used pg_advisory_lock (blocking, doesn't fit the boolean acquireLock contract) and interpolated the lock name into the SQL string (an injection footgun if copied). Rewrite to use pg_try_advisory_lock with parameterized queries via db.query(sql, params). Closes IMPLEMENTATION_REVIEW.md #11. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Working-tree-only spec and plan markdowns under docs/superpowers/ are gitignored-equivalent (untracked, never committed) but were tripping pnpm format:check locally. Adding the directory to .prettierignore keeps pnpm format:check clean without forcing the superpowers tooling to format-pass before every commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously runOperation called mutex.acquireLock() unconditionally on every dequeued operation. Manual lock management with autoreleaseLock: false (per docs/mutex.md:298-308) failed for non-idempotent mutex implementations like pg_try_advisory_lock, because the second acquire would throw or return false. Now runOperation checks isAcquired() first, and tracks whether it acquired in this call (acquiredHere) so it only releases what it acquired. Manual acquire+triggerEvent+release composes correctly with non-idempotent mutexes. Replaces the prior "does not short-circuit when isAcquired returns true" regression test, whose contradictory fake mutex (acquireLock returning false while isAcquired returns true) encoded an invariant that this fix intentionally inverts. The new test uses a realistic non-idempotent mutex that throws on a redundant acquire. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…less of name The previous validateNoConflictingDuplicates required BOTH different identity AND different name to flag a conflict. Two CallbackCondition objects sharing the name "shared" but encoding different logic were therefore silently deduplicated — the original review #6 bug. The regression test at tests/process-builder.test.ts:181 declared this case but tested a different one (different names), so it never exercised the bug. Now: same condition reference dedups; different references conflict. The buildAllStates dedup-key likewise uses object identity, not name. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the event-bound dispatcher (which runs CallbackObserver and similar commands attached to Event instances via event.attach()) was gated on currentState !== target, meaning self-transitions and false-condition matched events both skipped the dispatcher entirely. This contradicted docs/observers.md (which builds the OnEnterObserver example around a self-transition firing a CallbackObserver) and broke the simple "trigger event => command runs" mental model. Move the dispatcher to the top of processOperation, where it fires once per user-supplied event, before transition selection. Automatic transitions in the iteration loop don't carry an event, so no re-firing. State-machine before/after observers remain gated on real state change. The previously-passing test "should not execute observers on self-transition" was inverted: event observers must fire on self-transition. After-observers still don't fire (no state change). Adds a regression test for the false-condition case. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Welcome to Codecov 🎉Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests. Thanks for integrating Codecov - We've got you covered ☂️ |
The "Condition Name as Identity" section described the v2/pre-release
contract where same-name conditions silently deduplicated. v3 dedups by
object identity: same reference → silent dedup; different references →
DuplicateTransitionError, regardless of getName() match. The example
that showed two CallbackCondition("isReady", ...) instances merging
silently has been replaced with one showing the new conflict-throws
behavior alongside the same-reference idempotency case.
The CallbackCondition parameter table no longer claims the name is
"used as identity for deduplication" — it's only used for graph labels
and error messages now.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The DuplicateTransitionError description previously said duplicates were detected by "different condition objects with different names" and that "same condition name" silently deduplicated. v3 dedups by object reference identity, not by name: same reference dedups, any two different references conflict (regardless of whether they share a getName() value). Cross-link to the conditions guide for rationale. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Section 7 of the v2-to-v3 migration guide previously stated that v3 "always delegates to mutex.acquireLock()" with "no isAcquired() short-circuit". That was the v3 design intent at one point but the shipped v3 contract (per the review-fixes wave) is the opposite: runOperation consults isAcquired() first and skips the redundant acquireLock() when the mutex is already held. Rewrite the section to describe what v3 actually does, including a worked example of manual acquireLock/triggerEvent/releaseLock composing correctly with non-idempotent mutex implementations like pg_try_advisory_lock and redis SET NX. The "no short-circuit" claim is gone. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR publishes @camcima/finita v3.0.0, replacing the v2 graph construction/execution model with a builder-driven frozen graph, a queue-based execution engine, and a typed error hierarchy, with broad test + documentation updates to match the new APIs.
Changes:
- Introduces
ProcessBuilder(two-phase build) and internal construction guards to ensure immutable, builder-only graph creation. - Rewrites
Statemachineto serialize operations via a FIFO queue, split observers into before/after phases with immutable frames, and harden mutex behavior. - Adds
FinitaError+ typed error classes and updates tests/docs/CI for v3.
Reviewed changes
Copilot reviewed 79 out of 80 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/util.test.ts | Removes v2 utility helper tests (SetupHelper/StateCollectionMerger). |
| tests/typed-throws.test.ts | Adds tests asserting new typed error shapes/codes. |
| tests/selector.test.ts | Updates selector tests to build transitions via ProcessBuilder. |
| tests/process-builder.test.ts | Adds coverage for builder validation, dedup/conflict rules, and frozen graph constraints. |
| tests/observer.test.ts | Updates observer tests for v3 before/after observers and queued on-enter behavior. |
| tests/observer-frame.test.ts | Adds tests for frame immutability, ordering, veto behavior, and error aggregation. |
| tests/mutex.test.ts | Adds regression test for already-acquired mutex behavior during operations. |
| tests/integration.test.ts | Updates integration tests to ProcessBuilder and new Statemachine options signature. |
| tests/graph.test.ts | Updates graph tests to v3 graph construction patterns. |
| tests/graph-edge-dedup.test.ts | Adds test ensuring GraphBuilder.addState is idempotent for edges. |
| tests/generics.test.ts | Updates generics tests to builder-based graph creation. |
| tests/filter.test.ts | Updates filter tests to builder-based transitions and v3 state/event behavior. |
| tests/factory.test.ts | Updates factory tests to v3 before/after observer APIs and builder-based processes. |
| tests/exception-cleanup.test.ts | Updates exception/cleanup tests to queue engine + before/after observer model. |
| tests/error.test.ts | Updates error tests for new error types and builder-based duplicate state behavior. |
| tests/error-hierarchy.test.ts | Adds tests for FinitaError inheritance and unique codes. |
| tests/concurrency.test.ts | Adds tests for same-instance serialization and mutex acquisition semantics. |
| tests/builder-whitespace.test.ts | Adds tests for stricter event/condition name validation codes. |
| src/util/index.ts | Removes v2 util exports (placeholder empty module for v3). |
| src/util/StateCollectionMerger.ts | Removes v2 merger utility implementation. |
| src/util/SetupHelper.ts | Removes v2 setup helper implementation. |
| src/selector/OneOrNoneActiveTransition.ts | Throws typed AmbiguousTransitionError instead of generic Error. |
| src/observer/TransitionLogger.ts | Ports logger to AfterTransitionObserver + frame-based context. |
| src/observer/StatefulStatusChanger.ts | Ports status changer to AfterTransitionObserver with explicit subject injection. |
| src/observer/OnEnterObserver.ts | Ports on-enter to queued chaining via EnqueueContext. |
| src/observer/CallbackObserver.ts | Clarifies role as legacy event observer (not statemachine observer). |
| src/internal/OperationQueue.ts | Adds FIFO queue for serializing top-level statemachine operations. |
| src/internal/InternalConstruction.ts | Adds symbol guard to prevent user construction of core graph objects. |
| src/internal/Dispatcher.ts | Moves dispatcher under internal/ and updates interface imports. |
| src/interfaces/index.ts | Exports new before/after observer and frame types plus StatemachineOptions. |
| src/interfaces/TransitionFrameInterface.ts | Adds immutable TransitionFrame / ProposedTransitionFrame definitions. |
| src/interfaces/StatemachineOptions.ts | Adds options object type for Statemachine constructor. |
| src/interfaces/StatemachineInterface.ts | Updates interface for v3 observer APIs and removes v2 transient getters. |
| src/interfaces/StateInterface.ts | Removes mutating methods from state interface (builder-only graph mutations). |
| src/interfaces/FactoryInterface.ts | Updates factory interface to before/after observer registration. |
| src/interfaces/BeforeTransitionObserverInterface.ts | Adds before-observer interface (veto semantics). |
| src/interfaces/AfterTransitionObserverInterface.ts | Adds after-observer interface + enqueue handle and error semantics. |
| src/index.ts | Updates public surface: exports ProcessBuilder, new error types, new observer/frame types; drops util exports. |
| src/graph/GraphBuilder.ts | Makes addState idempotent for edges (prevents duplicate edge accumulation). |
| src/factory/StatefulStateNameDetector.ts | Throws typed InvalidSubjectError instead of generic Error. |
| src/factory/Factory.ts | Updates factory to v3 statemachine options signature and before/after observer attachment. |
| src/factory/AbstractNamedProcessDetector.ts | Throws typed ProcessNotFoundError with available processes. |
| src/error/index.ts | Exports new typed error hierarchy and related types. |
| src/error/WrongEventForStateError.ts | Migrates to FinitaError with code. |
| src/error/StateNotFoundError.ts | Adds typed error for missing state. |
| src/error/StateEventNotFoundError.ts | Adds typed error for missing event on a state. |
| src/error/ProcessNotFoundError.ts | Adds typed error for missing named process in detector. |
| src/error/ProcessFinalizedError.ts | Adds typed error for reusing a builder after build. |
| src/error/LockCanNotBeAcquiredError.ts | Migrates to FinitaError with code. |
| src/error/InvalidSubjectError.ts | Adds typed error for detector subject contract violations. |
| src/error/GraphValidationError.ts | Adds typed build-time validation error with code and details. |
| src/error/FinitaError.ts | Adds base abstract error class with discriminant code. |
| src/error/DuplicateTransitionError.ts | Adds typed conflict error for duplicate transition declarations. |
| src/error/DuplicateStateError.ts | Migrates to FinitaError with code. |
| src/error/AutomaticTransitionCycleError.ts | Adds typed error for automatic transition cycles. |
| src/error/AmbiguousTransitionError.ts | Adds typed selector error for multiple active transitions. |
| src/Transition.ts | Makes transitions builder-constructible only; freezes weight as readonly. |
| src/Statemachine.ts | Rewrites engine: FIFO op queue, frames, before/after observers, mutex handling, typed errors. |
| src/StateCollection.ts | Makes collection read-only, constructor-based, and throws StateNotFoundError. |
| src/State.ts | Makes states builder-constructible only; events declared up-front; transition init is internal. |
| src/ProcessBuilder.ts | Adds two-phase builder, validation, dedup/conflict rules, strict orphan detection. |
| src/Process.ts | Makes process builder-constructible only; stores states via read-only StateCollection; freezes process. |
| package.json | Bumps to 3.0.0; adds prepublishOnly; sets Node engine >=20. |
| docs/utilities.md | Updates utilities doc to reflect v3 removal of util helpers. |
| docs/selectors.md | Updates examples to ProcessBuilder and new Statemachine options. |
| docs/mutex.md | Updates constructor examples to options object; improves DB adapter example. |
| docs/migration/v2-to-v3.md | Adds v2→v3 migration guide (new APIs, behavior changes). |
| docs/interfaces.md | Updates interface docs for v3 observer/frame model. |
| docs/graph.md | Updates graph examples to builder-based construction. |
| docs/factory.md | Updates factory docs/examples toward v3 patterns (some API references remain). |
| docs/errors.md | Updates errors docs for new typed errors and codes. |
| docs/conditions.md | Updates condition examples toward builder-based construction. |
| README.md | Updates badges, highlights ProcessBuilder, and updates examples for v3. |
| .prettierignore | Ignores docs/superpowers/. |
| .github/workflows/ci.yml | Updates CI Node matrix to 20/22/24 and adds npm pack --dry-run job. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The README links to docs/*.md with relative paths, but package.json "files" only includes dist, README.md, LICENSE, and package.json — docs are intentionally excluded from the published tarball to keep install size down. Relative links therefore 404 from npmjs.com and from any consumer reading the README out of node_modules. Switch the documentation table of contents to absolute URLs pointing at github.com/camcima/finita/blob/main/docs/. They resolve correctly from npmjs.com, from cloned repos, and from the GitHub web view. The in-repo navigation experience is unchanged (GitHub renders absolute self-links the same as relative ones in the README preview). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Follow-up review surfaced three doc-drift issues + one packaging issue. All addressed in commits
Gates after the four commits:
🤖 Generated with Claude Code |
The docs/superpowers/ directory holds working-tree-only spec and plan markdowns produced by the superpowers tooling — design notes, agent prompts, and intermediate planning state for individual development waves. Two files from the original execution/graph redesign wave were accidentally committed early on; the rest of the wave's artifacts have been kept untracked since. Add docs/superpowers/ to .gitignore so future planning artifacts can live alongside the code without cluttering the repository, and untrack the two committed files (kept on disk locally, just removed from the git index). The .prettierignore entry from the earlier sub-project E fix stays — it's belt-and-suspenders for tooling that doesn't honor .gitignore. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…etBeforeObservers The pre-existing veto-by-throw test exercised only the path where a BeforeTransitionObserver throws and aborts the transition. Adds: - Happy-path test: a single before-observer notify completes successfully, sees a frozen ProposedTransitionFrame, and the transition commits. - Multi-observer ordering test: three before-observers attached in order, the second throws, the third never runs — confirms the loop short-circuits on the first throw and respects attach order. - detachBefore / detachAfter: attach an observer, transition once, detach, transition again, assert the observer was not re-invoked. Also exercises detaching an observer that was never attached (must be a no-op). - getBeforeObservers: empty initially, returns attached observers in attach order, reflects detachments. Closes the Statemachine.ts coverage gap on lines 88-90, 93-94, 101-103, 280. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… factory Factory.ts had 55% function coverage before this change because five public methods were never tested: - attachBeforeObserver / detachBeforeObserver — analogous to the already-tested after-observer pair, asserts the observer is propagated to (or absent from) the statemachine returned by createStatemachine. - setTransitionSelector — a custom ScoreTransition selector reaches the configured transition and the resulting statemachine transitions correctly under it. - setMutexFactory — given a stateful subject, the factory invokes the mutex factory with the per-subject resource string, the resulting statemachine acquires and releases that resource through the configured adapter exactly once per operation. Includes the setMutexFactory(null) clearing path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The two pre-existing strictOrphans tests both built graphs where the initial state had no transitions, so the BFS reachability loop body in validateOrphans (ProcessBuilder.ts:333-335) was never executed. Adds two cases that exercise the traversal: - "strictOrphans walks transitions and rejects states unreachable through them" — three reachable states (a -> b -> c) plus a fourth lonely state with no incoming transition. The BFS must walk from a to b to c, and the orphan check must correctly identify "lonely" as the only unreachable state. - "strictOrphans accepts a fully reachable graph" — same a -> b -> c topology without the orphan, builds successfully. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GraphBuilder.getTransitionLabel collects the names of observers attached to a transition's event and renders them as "C: <names>" inside the transition label. The pre-existing graph tests built transitions whose events had no attached observers, so this whole block (the for-loop over event.getObservers and the C: prefix construction) was uncovered. The Named-detection helpers (isNamed, convertToString) were also unreached because they're only used for this rendering. Adds three cases: - Named observer (has getName()) → label contains "C: <name>". - Multiple observers in attach order, comma-joined. - Non-Named observer (only update()) → label uses the String(obj) fallback via toString(). Closes the GraphBuilder.ts coverage gap on lines 24-31, 51-56, 89-96. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The multi-observer test passed inline object literals with getName + update properties to event.attach(), which type-checks the literal strictly against Observer (which has only update). Pulling the observers into named consts widens the literal type before the call and lets TypeScript accept them. Test behavior unchanged. A typecheck slipped through on the previous commit because the test suite runs without TypeScript strict-mode for tests; pnpm lint catches it via tsconfig.test.json. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Coverage analysis surfaced dead code on Dispatcher: - isReady() — defined on the class and on DispatcherInterface, but has zero callers in src/ or tests/. Removed from both. - onReadyCallback (3rd param of dispatch) plus the onReadyCallbacks field and the trailing invoke loop — the only call site (Statemachine.ts:233) passes two arguments, so the optional callback path was unreachable. Removed from both the class and the interface. CallbackInterface stays exported because DispatcherInterface still extends it (the parent only declares invoke(), which is what a "callback" is). Dispatcher itself is internal (lives in src/internal/ and is not exported from the package entry point), so the runtime class shape change is invisible to consumers. Folds into v3.0.0 since the unshipped version is the only place this shape was ever public — no users to migrate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Coverage analysis pass — 96.09% → 98.62% statements, 95.13% → 99.55% functions (test count 263 → 278, +15 tests). Closed gaps:
Remaining gaps (all intentional / structural):
Gates: 278/278 tests, lint clean, build clean, format clean. 🤖 Generated with Claude Code |
OperationQueue.size() has zero callers in src/ and tests/, and OperationQueue itself is not exported from the package entry point (it lives in src/internal/ and is consumed only by Statemachine). There is no OperationQueueInterface — the class is pure internal plumbing — so removing the method has no consumer impact. Folds into v3.0.0 alongside the Dispatcher cleanup. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ore/after API Three documentation surfaces still referenced the v2 unified-observer methods that were removed in the v3 rewrite: - docs/factory.md "Methods" table listed attachStatemachineObserver, detachStatemachineObserver, getStatemachineObservers — none of which exist on the v3 Factory class. Replaced with the four v3 methods: attach/detachBeforeObserver, attach/detachAfterObserver. - docs/factory.md two example snippets called factory.attachStatemachineObserver(...) for StatefulStatusChanger, TransitionLogger, and OnEnterObserver — all three are AfterTransitionObserver implementations, so the calls become attachAfterObserver. - docs/interfaces.md FactoryInterface snippet showed the same removed methods. Replaced with the actual interface shape from src/interfaces/FactoryInterface.ts (no get* method — Factory doesn't expose its observer collections). Surfaced by GitHub Copilot's review of PR #17. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codecov was running with default behavior (no config file present), which enabled both project and patch status checks with auto targets — meaning every PR diff was independently graded for coverage and could fail CI on small dips even when overall coverage stayed healthy. Add a codecov.yml that: - Sets the project status target to a fixed 80% floor. As long as total coverage stays at or above 80%, the project check passes, regardless of whether a particular commit slightly reduces it. Current coverage sits at 98.62% statements, leaving plenty of headroom. - Disables the patch (per-diff) status entirely. PRs that don't add tests for a small change won't fail Codecov on patch coverage; the project floor remains the single coverage gate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
This PR ships v3.0.0 of
@camcima/finita. It is a major rewrite landing across 55 commits in five waves (A+B, C, D, E plus follow-ups). The core change is replacing the v2 ad-hoc graph wiring with aProcessBuilder-driven, two-phase frozen graph and a rewritten execution engine; everything else flows from that.Wave A+B — graph + execution rewrite
ProcessBuildertwo-phase build: everyTransitiontargets a realStateinstance, even under cycles and self-loops (fix(builder): correct transition target identity under cycles).Statemachineexecution engine rewritten to pump operations through a FIFO queue, separating the active-transition filter from the transition selector and properly modeling the lifecycle phases (proposed → before-observers → commit → after-observers → optional auto-chain).BeforeTransitionObserverandAfterTransitionObserverwithTransitionFrame/ProposedTransitionFrameimmutable payloads.OnEnterObserver(now queue-driven),TransitionLogger,StatefulStatusChanger,CallbackObserver,Factory.SetupHelperandStateCollectionMergerremoved.Wave C — typed error hierarchy
FinitaErrorbase class withcodediscriminant.ProcessFinalizedError,GraphValidationError,DuplicateTransitionError,StateNotFoundError,StateEventNotFoundError,ProcessNotFoundError,InvalidSubjectError,AmbiguousTransitionError,AutomaticTransitionCycleError.GraphBuilder.addStatemade idempotent (closes build(deps-dev): bump release-it from 19.2.4 to 20.0.0 #14).Wave D — packaging and CI
prepublishOnlyhook +engines.noderange tightened.npm pack --dry-runverification gate.Wave E — review-driven bug fixes (this final wave)
Four targeted fixes uncovered during post-D review, each landing as one commit:
fix(mutex): runoperation honors an already-held mutex—runOperationnow skipsacquireLock()whenisAcquired()returns true, with a newacquiredHerelocal that ensures we only release what this call acquired. Manual lock management withautoreleaseLock: falsenow composes correctly with non-idempotent mutexes (`pg_try_advisory_lock`, redis `SET NX`).fix(builder): treat different condition instances as conflicts regardless of name—validateNoConflictingDuplicatesnarrowed to compare condition references only; same reference dedups, different references throwDuplicateTransitionError.buildAllStatesdedup-key likewise switched to per-builder object identity. Closes original review chore(deps-dev): bump @vitest/coverage-v8 from 3.2.4 to 4.1.2 #6.fix(statemachine): event observers fire on event resolution— event-bound dispatcher hoisted out of thecurrentState !== targetbranch to the top ofprocessOperation. Event observers now fire on self-transitions and false-condition matched events, matching the documented Observer Lifecycle. State-machine before/after observers remain gated on real state change.chore: ignore docs/superpowers/ in prettier— working-tree-only spec/plan markdowns no longer trippnpm format:check.Breaking changes from v2
ProcessBuilderand frozen before use. DirectaddState/addTransitionon graphs is gone. Seedocs/migration/v2-to-v3.md.TransitionFrame/ProposedTransitionFrame. Event-attached observers fire whenever the event is resolved on the current state (including self-transitions and false-condition matches).CallbackCondition("foo", ...)instances with different bodies now throwDuplicateTransitionErrorinstead of silently merging.FinitaErrorand carry acodediscriminant. v2 error classes have been replaced with typed counterparts.GraphValidationErrorat build time.Quality gates
pnpm test— 263 tests across 19 filespnpm lint— clean (tsc --noEmit && tsc --project tsconfig.test.json && eslint src/ tests/)pnpm build— clean ESM + CJS + DTSpnpm format:check— cleannpm pack --dry-run— 76.9 kB tarball, 9 filesKnown follow-up before publishing v3.0.0 to npm
The Wave E final review surfaced documentation drift in three files that still describe pre-F2 behavior. Not merge-blockers, but they must land before `npm publish`:
Test plan
Note on origin/main divergence
`origin/main` has 4 dependabot dev-dependency bumps not in this branch. They look mechanical (release-it 19→20, dev-dependency group bump). Resolve at merge time — likely a clean rebase or a no-conflict merge.
🤖 Generated with Claude Code