aifsmjs 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/llms-full.txt CHANGED
@@ -19,9 +19,9 @@ The short index lives at `llms.txt` (see https://llmstxt.org/).
19
19
  [![AI Generated](https://img.shields.io/badge/AI_Generated-Claude_Code_Opus_4.7_Max-blueviolet.svg)](https://www.anthropic.com/claude-code)
20
20
  [![繁體中文](https://img.shields.io/badge/lang-繁體中文-red.svg)](README_ZHTW.md)
21
21
 
22
- > A small, strict FSM library built for **web game development**. Lifecycle is a pure `step()` function. Chain-of-Responsibility intuition is reserved for cross-cutting concerns (observe / persist / replay), never for the transition core.
22
+ > A small, strict FSM library for any TypeScript/JS app that needs deterministic, replayable state transitions. Lifecycle is a pure `step()` function. Chain-of-Responsibility intuition is reserved for cross-cutting concerns (observe / persist / replay), never for the transition core.
23
23
 
24
- **Primary audience**: developers building browser-based games and interactive web experiences on PixiJS / Svelte 5 / plain Canvas / WebGL. Typical use cases: scene flow (loading menu playing result), character AI state, interactive UI flows, tutorial steps, turn-based logic. The library itself is **environment-neutral** (pure core + adapter boundary) browser, Node, and Flutter WebView all work; games are simply the first-class use case.
24
+ **Primary audience**: developers building stateful flows multi-step forms, checkout funnels, auth flows, tutorial sequences, document-status workflows, scene flow in interactive apps, and the same patterns in browser-based games (PixiJS / Svelte 5 / plain Canvas / WebGL). The core is **environment-neutral** (pure function + adapter boundary): browser, Node, Bun, Deno, Flutter WebView, and Web Workers all work. The Roadmap section keeps gaming-specific niceties (tick hook, ECS bridge) as opt-in subpaths, not core surface.
25
25
 
26
26
  > Traditional Chinese version: [README_ZHTW.md](README_ZHTW.md).
27
27
 
@@ -120,7 +120,7 @@ The three layers are fully decoupled: take `step()` alone for replay, take `Mach
120
120
  | Will do (v1) | Won't do |
121
121
  | --------------------------------------------------- | ------------------------------------------------- |
122
122
  | Flat states + transitions | Hierarchical / compound states |
123
- | Guards (sync only) | Async guards |
123
+ | Guards (sync only; inline async throws `InvalidDefinitionError` at `defineMachine`; runtime throws `AsyncGuardError` on thenable return) | Async guards |
124
124
  | Actions (assign + enqueue effects) | Async API inside an action (use an effect) |
125
125
  | Fire-and-forget effects | Actor invocation / spawn |
126
126
  | Read-only inspect middleware | Cancellable transition middleware |
@@ -510,6 +510,66 @@ All notable changes to this project will be documented in this file.
510
510
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
511
511
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
512
512
 
513
+ ## [0.2.0] — 2026-05-28
514
+
515
+ ### Added
516
+
517
+ - **Async-guard detection**: `evalGuard` and `defineMachine`'s validation pass now throw on async guards. TypeScript already prevents the typed case, but a JS caller or a cast could slip an async guard through and silently pass every check (a thenable is truthy). The new check fails loudly:
518
+ - **Definition time** (inline `async` guard) → `InvalidDefinitionError` from `defineMachine`'s `validateDefinition`.
519
+ - **Runtime** (string-ref or cast guard whose return value is thenable) → `AsyncGuardError` from `evalGuard`. The thenable check uses `typeof x?.then === "function"`, so cross-realm Promises (iframe / worker / vm) and user-defined PromiseLike values are also caught — not just same-realm `instanceof Promise`.
520
+ - New exports from `aifsmjs`: `AsyncGuardError`, `isAsyncGuardFn`.
521
+ - README's "Capabilities / Limitations" table updated to reflect the new runtime guarantee.
522
+
523
+ ### Fixed (correctness)
524
+
525
+ - **`send()` transition payload AND `notify()` listeners are captured pre-reentry** ([src/fsm/runtime.ts](src/fsm/runtime.ts)): the `next` field of the emitted `'transition'` event, the effect dispatch context, and the snapshot delivered to `subscribe()` listeners are all now read from a captured local `committed` snapshot rather than the outer mutable `snapshot` variable. Closes a race where a reentrant `send()` inside an effect handler or subscriber would race ahead and the outer payload / later subscribers in the same notify pass would end up pointing at the reentry's snapshot.
526
+ - **`evalGuard` falls back to `<inline>` for anonymous-arrow guards** ([src/fsm/evaluator.ts](src/fsm/evaluator.ts)): switched `??` to `||` so an empty `Function.prototype.name` falls back instead of producing `guard "" must be sync;`.
527
+ - **README + README_ZHTW + llms-full.txt** now describe the two error paths separately (`InvalidDefinitionError` at definition time vs `AsyncGuardError` at runtime) instead of conflating them.
528
+
529
+ ### Changed (positioning + meta)
530
+
531
+ - **`package.json#description`** rewritten from «for web game development» to lead with the broader use case set (multi-step forms, checkout funnels, auth flows, tutorials, scene flow). The README's "Primary audience" paragraph already moved away from game-only framing in v0.2.0; the package metadata now matches.
532
+
533
+ ### Build & tooling
534
+
535
+ - **`verify:llms` is now build-agnostic** ([scripts/build-llms-full.mjs](scripts/build-llms-full.mjs)): the script accepts `--check` which builds the file in memory and compares against disk, exit 1 on diff. The previous form used `git diff --exit-code -- llms-full.txt` after running the build, which failed any time the working tree had uncommitted changes (not just llms-full.txt drift). The new form works identically pre-commit and in CI.
536
+ - **Per-subpath gzip budgets raised** ([scripts/check-size.mjs](scripts/check-size.mjs)): core 3500 → 3700 B and replay 1600 → 1800 B to absorb the AsyncGuardError + thenable detection cost; pbt 4500 → 4600 B for a small symbol additions. All entries still tracked at ≥95% headroom.
537
+
538
+ ### Added (examples)
539
+
540
+ - `examples/03-checkout-funnel` — e-commerce checkout funnel with guarded staging, payment / analytics effects, and a `replay()` round-trip. Demonstrates that aifsmjs models classic web UX flows with no canvas / game loop involvement.
541
+ - `examples/04-form-wizard` — multi-step form wizard with back / next / jump-to-step navigation, per-step validation, and draft persistence via the `persist` middleware.
542
+
543
+ ### Changed (positioning)
544
+
545
+ - `README.md` and `README_ZHTW.md`'s "Primary audience" paragraph now leads with stateful web flows (multi-step forms, checkout funnels, auth flows, tutorials, document workflows) and frames games as one application of the same pattern. The core remains environment-neutral; the only opt-in dependency is `fast-check` for the `aifsmjs/pbt` PBT adapter.
546
+
547
+ ### Compatibility
548
+
549
+ This release is **non-breaking at runtime** for users who already wrote sync guards. Async guards previously slipped through and silently passed; they now throw. If you relied on this accidental behaviour, move the async work into an effect (`enq.effect(...)`) and dispatch a follow-up event when the work completes — the pattern is documented in the README's "Common pitfalls" table.
550
+
551
+ ## [0.1.2] — 2026-05-28
552
+
553
+ ### Fixed
554
+
555
+ - **Memory**: `runtime.on(type, fn, { signal })` previously left an
556
+ abort listener attached to the external `AbortSignal` after
557
+ `runtime.dispose()`. The listener (and its closure over the user's
558
+ callback) was retained until the signal eventually fired or was
559
+ garbage-collected. `dispose()` now removes each abort listener from
560
+ the signal it was attached to, and the unsubscribe function returned
561
+ by `on()` does the same on manual unsubscribe.
562
+
563
+ ### Internal
564
+
565
+ - Narrowed `dispatchEffects` event parameter from `Evt | ResetEvent`
566
+ to `Evt`; the function is only reached via `send()`. Removed the
567
+ corresponding `as Evt` cast.
568
+ - Removed a redundant `snapshot.context as Ctx` cast in `step()`.
569
+
570
+ No public API changes; existing 0.1.1 callers run unchanged. Core gzip
571
+ 3296 B → 3401 B (97% of 3500 B budget).
572
+
513
573
  ## [0.1.1] — 2026-05-28
514
574
 
515
575
  ### Changed
@@ -738,16 +798,20 @@ package).
738
798
 
739
799
  ```bash
740
800
  pnpm install
741
- pnpm example:traffic-light # 01-traffic-light
742
- pnpm example:approval # 02-approval-workflow
801
+ pnpm example:traffic-light # 01-traffic-light
802
+ pnpm example:approval # 02-approval-workflow
803
+ pnpm example:checkout-funnel # 03-checkout-funnel
804
+ pnpm example:form-wizard # 04-form-wizard
743
805
  ```
744
806
 
745
807
  ## Index
746
808
 
747
- | # | Example | What it shows | Maps to a web-game pattern |
809
+ | # | Example | What it shows | Real-world pattern |
748
810
  |---|---|---|---|
749
- | 01 | [traffic-light](01-traffic-light/index.ts) | Minimal `setup → defineMachine → createRuntime → send` loop with `assign` and a snapshot subscriber. | Cyclic scene flow (loading → menu → playing → result → loading) |
750
- | 02 | [approval-workflow](02-approval-workflow/index.ts) | Multi-candidate guarded transitions (`and([...])`), effects + handlers, `persist` + `recorder` middleware, and `replay()` to reproduce the final snapshot. | Turn-based logic with branching outcomes + post-mortem replay |
811
+ | 01 | [traffic-light](01-traffic-light/index.ts) | Minimal `setup → defineMachine → createRuntime → send` loop with `assign` and a snapshot subscriber. | Cyclic scene flow (loading → menu → playing → result → loading) — or any 3-state cycle. |
812
+ | 02 | [approval-workflow](02-approval-workflow/index.ts) | Multi-candidate guarded transitions (`and([...])`), effects + handlers, `persist` + `recorder` middleware, and `replay()` to reproduce the final snapshot. | Document approval, ticket triage, turn-based games with branching outcomes + post-mortem replay. |
813
+ | 03 | [checkout-funnel](03-checkout-funnel/index.ts) | E-commerce checkout: cart → shipping → payment → review → confirmed. Per-stage validation as guards, payment + analytics as effects, full replay of the funnel. | Plain web app — no canvas, no game loop. Demonstrates that aifsmjs models classic UX funnels. |
814
+ | 04 | [form-wizard](04-form-wizard/index.ts) | Multi-step form wizard with back / next / jump-to-step navigation. Per-step validation, draft persistence via the `persist` middleware. | Account onboarding, settings editor, multi-page survey. |
751
815
 
752
816
  ## Notes
753
817
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aifsmjs",
3
- "version": "0.1.1",
4
- "description": "Small, strict FSM library for web game development. Pure step() lifecycle, opt-in effects, inspect, replay, and a fast-check property-based testing adapter. Browser / Node / WebView friendly.",
3
+ "version": "0.2.0",
4
+ "description": "Small, strict FSM library for deterministic, replayable state machines in any TypeScript/JS app — multi-step forms, checkout funnels, auth flows, tutorials, scene flow. Pure step() lifecycle, opt-in effects, inspect, replay, and a fast-check property-based testing adapter. Browser / Node / Bun / Deno / WebView / Worker friendly.",
5
5
  "keywords": [
6
6
  "fsm",
7
7
  "state-machine",
@@ -88,10 +88,12 @@
88
88
  "verify:exports": "node scripts/verify-exports.mjs",
89
89
  "check:size": "node scripts/check-size.mjs",
90
90
  "build:llms": "node scripts/build-llms-full.mjs",
91
- "verify:llms": "node scripts/build-llms-full.mjs && git diff --exit-code -- llms-full.txt",
91
+ "verify:llms": "node scripts/build-llms-full.mjs --check",
92
92
  "coverage": "vitest run --coverage",
93
93
  "example:traffic-light": "tsx examples/01-traffic-light/index.ts",
94
94
  "example:approval": "tsx examples/02-approval-workflow/index.ts",
95
+ "example:checkout-funnel": "tsx examples/03-checkout-funnel/index.ts",
96
+ "example:form-wizard": "tsx examples/04-form-wizard/index.ts",
95
97
  "prepublishOnly": "pnpm typecheck && pnpm lint && pnpm coverage && pnpm build && pnpm verify:exports && pnpm check:size"
96
98
  },
97
99
  "peerDependencies": {