Skip to content

v3.0.0 — execution engine rewrite, typed errors, hardened builder - #17

Merged
camcima merged 69 commits into
mainfrom
v3-redesign
May 1, 2026
Merged

v3.0.0 — execution engine rewrite, typed errors, hardened builder#17
camcima merged 69 commits into
mainfrom
v3-redesign

Conversation

@camcima

@camcima camcima commented Apr 30, 2026

Copy link
Copy Markdown
Owner

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 a ProcessBuilder-driven, two-phase frozen graph and a rewritten execution engine; everything else flows from that.

Wave A+B — graph + execution rewrite

  • New ProcessBuilder two-phase build: every Transition targets a real State instance, even under cycles and self-loops (fix(builder): correct transition target identity under cycles).
  • Statemachine execution 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).
  • New observer surfaces: BeforeTransitionObserver and AfterTransitionObserver with TransitionFrame / ProposedTransitionFrame immutable payloads.
  • Legacy v2 components ported: OnEnterObserver (now queue-driven), TransitionLogger, StatefulStatusChanger, CallbackObserver, Factory. SetupHelper and StateCollectionMerger removed.

Wave C — typed error hierarchy

  • FinitaError base class with code discriminant.
  • New typed errors retrofit across the codebase: ProcessFinalizedError, GraphValidationError, DuplicateTransitionError, StateNotFoundError, StateEventNotFoundError, ProcessNotFoundError, InvalidSubjectError, AmbiguousTransitionError, AutomaticTransitionCycleError.
  • Stricter input validation: whitespace-padded event names rejected; empty/whitespace condition names rejected.
  • GraphBuilder.addState made idempotent (closes build(deps-dev): bump release-it from 19.2.4 to 20.0.0 #14).

Wave D — packaging and CI

  • prepublishOnly hook + engines.node range tightened.
  • CI matrix bumped to Node 20/22/24.
  • New CI job: npm pack --dry-run verification gate.
  • Docs refreshed across the surface (migration guide, selectors, mutex, README badges).

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 mutexrunOperation now skips acquireLock() when isAcquired() returns true, with a new acquiredHere local that ensures we only release what this call acquired. Manual lock management with autoreleaseLock: false now composes correctly with non-idempotent mutexes (`pg_try_advisory_lock`, redis `SET NX`).
  • fix(builder): treat different condition instances as conflicts regardless of namevalidateNoConflictingDuplicates narrowed to compare condition references only; same reference dedups, different references throw DuplicateTransitionError. buildAllStates dedup-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 the currentState !== target branch to the top of processOperation. 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 trip pnpm format:check.

Breaking changes from v2

  • API: state machines must be built via ProcessBuilder and frozen before use. Direct addState/addTransition on graphs is gone. See docs/migration/v2-to-v3.md.
  • Observers: before/after observers now operate on immutable TransitionFrame/ProposedTransitionFrame. Event-attached observers fire whenever the event is resolved on the current state (including self-transitions and false-condition matches).
  • Conditions: dedup is by object identity, not by name. Two CallbackCondition("foo", ...) instances with different bodies now throw DuplicateTransitionError instead of silently merging.
  • Errors: all framework errors now extend FinitaError and carry a code discriminant. v2 error classes have been replaced with typed counterparts.
  • Validation: whitespace-padded event names and empty/whitespace condition names now throw GraphValidationError at build time.

Quality gates

  • pnpm test — 263 tests across 19 files
  • pnpm lint — clean (tsc --noEmit && tsc --project tsconfig.test.json && eslint src/ tests/)
  • pnpm build — clean ESM + CJS + DTS
  • pnpm format:check — clean
  • npm pack --dry-run — 76.9 kB tarball, 9 files
  • ✅ Per-wave code review (A+B, C, D, E individually); plus a final cross-cutting review of Wave E confirmed APPROVED FOR MERGE
  • CI matrix: Node 20/22/24

Known 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`:

  • `docs/conditions.md:22-43` — "Condition Name as Identity" section needs rewrite (dedup is by reference identity now).
  • `docs/conditions.md:160` — `CallbackCondition` parameter table; drop "used as identity for deduplication".
  • `docs/errors.md:235` — `DuplicateTransitionError` description has both clauses inverted vs. the new contract.
  • `docs/migration/v2-to-v3.md:161` — sharpen "conflicting condition objects" → "different condition object identities".
  • CHANGELOG / migration — add a note about F1's event-observer-on-resolution semantic for v2 users porting.

Test plan

  • CI green on Node 20, 22, 24 (full matrix)
  • CodeQL clean
  • OSV-Scanner clean
  • `npm pack --dry-run` job green
  • Smoke test: install the packed tarball into a sample consumer and confirm imports + a basic state machine work
  • Doc-correction follow-up PR landed before `npm publish`

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

Carlos Cima and others added 30 commits April 30, 2026 10:57
- 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>
Carlos Cima and others added 8 commits April 30, 2026 15:56
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>
Copilot AI review requested due to automatic review settings April 30, 2026 22:53
@codecov

codecov Bot commented Apr 30, 2026

Copy link
Copy Markdown

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 ☂️

Carlos Cima and others added 3 commits April 30, 2026 18:57
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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Statemachine to 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.

Comment thread docs/interfaces.md Outdated
Comment thread docs/conditions.md Outdated
Comment thread docs/errors.md Outdated
Comment thread docs/migration/v2-to-v3.md Outdated
Comment thread docs/factory.md Outdated
Comment thread docs/factory.md Outdated
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>
@camcima

camcima commented Apr 30, 2026

Copy link
Copy Markdown
Owner Author

Follow-up review surfaced three doc-drift issues + one packaging issue. All addressed in commits 51b4c6f..8d77d3b:

  • 51b4c6f docs(conditions) — rewrite "Condition Identity" section to describe v3's object-identity dedup with both an idempotent same-reference example and a same-name-different-reference conflict example. Drop "used as identity for deduplication" from the CallbackCondition parameter table.
  • dc36563 docs(errors)DuplicateTransitionError description now states the contract correctly: same reference dedups, different references conflict regardless of getName(). Cross-link to the conditions guide.
  • fb7dd7d docs(migration) — section 7 of the migration guide was 180° wrong (claimed "no isAcquired() short-circuit" while v3 explicitly added one in F3). Rewrote with a worked example of manual acquireLock / triggerEvent / releaseLock composing with non-idempotent mutexes.
  • 8d77d3b docs(readme) — relative docs/*.md links 404 from npmjs.com and from node_modules because package.json files excludes docs. Switched to absolute github.com/camcima/finita/blob/main/docs/* URLs.

Gates after the four commits:

  • pnpm test — 263 tests / 19 files passing
  • pnpm lint — clean
  • pnpm format:check — clean
  • No code changes; runtime behavior unchanged.

🤖 Generated with Claude Code

Carlos Cima and others added 7 commits May 1, 2026 05:42
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>
@camcima

camcima commented May 1, 2026

Copy link
Copy Markdown
Owner Author

Coverage analysis pass — 96.09% → 98.62% statements, 95.13% → 99.55% functions (test count 263 → 278, +15 tests).

Closed gaps:

  • 5edd49a test(observer-frame) — happy-path BeforeTransitionObserver, multi-observer ordering, detachBefore/detachAfter, getBeforeObservers (Statemachine.ts:88-103, 280)
  • 731b203 test(factory) — attach/detachBeforeObserver, setTransitionSelector, setMutexFactory + null-clearing path; Factory function coverage 55% → 100%
  • b27db7b test(process-builder) — strictOrphans BFS traversal exercised with reachable + unreachable states (ProcessBuilder.ts:333-335)
  • c2683ba test(graph) + b16edab test(graph) typing fix — event-observer labels with Named, multi-observer, and non-Named fallback paths; GraphBuilder 88% → 100%
  • 502e290 refactor(dispatcher) — dropped isReady() method and onReadyCallback parameter; both had zero callsites in src/ or tests/, and Dispatcher is internal so no consumer impact. CallbackInterface retained.

Remaining gaps (all intentional / structural):

  • Constructor-key defensive throws in Process, State, Transition — testing would require illegal construction.
  • Empty catch comment in Statemachine.runOperation finally-block — non-executable.
  • Dispatcher "Was already invoked!" guards — defensive against API misuse on a class that's only used internally.
  • MaybePromise.ts (type alias) and util/index.ts (export {} placeholder) — no executable code.
  • A couple of ?? fallback branches in error message formatters (cosmetic).

Gates: 278/278 tests, lint clean, build clean, format clean.

🤖 Generated with Claude Code

Carlos Cima and others added 3 commits May 1, 2026 06:11
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>
@camcima
camcima merged commit d7c9e8b into main May 1, 2026
8 checks passed
@camcima
camcima deleted the v3-redesign branch May 1, 2026 10:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants