aifsmjs 0.2.1 → 0.3.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
@@ -119,15 +119,16 @@ The three layers are fully decoupled: take `step()` alone for replay, take `Mach
119
119
 
120
120
  | Will do (v1) | Won't do |
121
121
  | --------------------------------------------------- | ------------------------------------------------- |
122
- | Flat states + transitions | Hierarchical / compound states |
122
+ | Flat states + transitions | Parallel state regions |
123
+ | Hierarchical sugar via `state.sub` (experimental, since 0.3.0) | Closures embedded in definition (breaks serialize) |
123
124
  | Guards (sync only; inline async throws `InvalidDefinitionError` at `defineMachine`; runtime throws `AsyncGuardError` on thenable return) | Async guards |
124
125
  | Actions (assign + enqueue effects) | Async API inside an action (use an effect) |
125
126
  | Fire-and-forget effects | Actor invocation / spawn |
126
127
  | Read-only inspect middleware | Cancellable transition middleware |
127
128
  | `replay(initial, log, def, impl)` pure function | Time-travel debugger (v2 candidate) |
128
129
  | `fast-check` `fc.commands` adapter | Custom PBT framework |
129
- | String ref + runtime injection | Closures embedded in definition (breaks serialize) |
130
- | Tree-shake friendly subpath exports | Single root import for everything |
130
+ | String ref + runtime injection | Single root import for everything |
131
+ | Tree-shake friendly subpath exports | ECS / Pixi bridges (opt-in subpath, not core) |
131
132
 
132
133
  ---
133
134
 
@@ -369,6 +370,27 @@ Non-goals:
369
370
  - No async lifecycle hook
370
371
  - Inspect middleware cannot alter the transition outcome
371
372
 
373
+ ### Sub-machine lifecycle (since 0.3.0, experimental)
374
+
375
+ When a state declares `sub`, the per-transition ordering is:
376
+
377
+ 1. Parent `step()` runs: `exit actions → transition.actions → entry actions`.
378
+ 2. Old child (if any) `dispose()` — synchronous; exceptions become
379
+ `SubMachineError(phase: "dispose")`.
380
+ 3. New child (if next state has `sub`) instantiation — exceptions become
381
+ `SubMachineError(phase: "init")`.
382
+ 4. Parent snapshot commits.
383
+ 5. Middleware pipeline runs.
384
+ 6. Effects dispatch.
385
+ 7. `'transition'` event emits to `on()` / `onTransition()` subscribers.
386
+
387
+ If step 2 or 3 throws, the parent snapshot is **not** committed (rollback
388
+ to `prev`); no middleware / effects / `'transition'` runs.
389
+
390
+ `runtime.dispose()` cascades to the child via `controller.signal`'s abort
391
+ listener and an explicit `child.dispose()` call. Cascade swallows child
392
+ exceptions to honour the never-throws dispose contract.
393
+
372
394
  ---
373
395
 
374
396
  ## Lifecycle Protocol
@@ -451,7 +473,7 @@ Example-first, PBT-augmented. Lesson from jssm: "3000+ tests / 100% coverage" tu
451
473
  - **Example tests** (vitest): for every src module, write happy path + edge + error-message triplets.
452
474
  - **PBT smoke**: each generic property runs 50 iterations as an invariant guard, not as a coverage source.
453
475
  - **CI-enforced thresholds**: `@vitest/coverage-v8` is wired to **100% statements / 100% lines / 100% functions / ≥90% branches**. The few defensive invariant-guard branches (e.g. runtime determinism mismatch) carry `/* v8 ignore */` annotations with rationale.
454
- - **Size budget**: `scripts/check-size.mjs` enforces per-subpath gzip caps in CI — core ≤3 KB, replay ≤1.6 KB, pbt ≤4.5 KB, others ≤1 KB. Exceeding any cap fails the build.
476
+ - **Size budget**: `scripts/check-size.mjs` enforces per-subpath gzip caps in CI — core ≤4.7 KB (raised in 0.3.0 for sub-machine sugar), replay ≤1.8 KB, pbt ≤5.5 KB (raised in 0.3.0 because `pbt` transitively imports `createRuntime`), others ≤1 KB. Exceeding any cap fails the build.
455
477
 
456
478
  ### The 6 built-in generic properties
457
479
 
@@ -471,7 +493,7 @@ Example-first, PBT-augmented. Lesson from jssm: "3000+ tests / 100% coverage" tu
471
493
  | | aifsmjs | XState v5 | Robot3 | @xstate/store | Zag.js |
472
494
  | -------------------------- | -------------- | ----------------- | ----------------- | ----------------- | ----------------- |
473
495
  | Core size (gzip) | ~2.8KB | ~15KB | ~1KB | < 1KB | per-component |
474
- | Hierarchical states | No (v1) | Yes | No | N/A | Yes |
496
+ | Hierarchical states | Sugar (0.3.0) | Yes | No | N/A | Yes |
475
497
  | Async invoke / actor | No | Yes | No | N/A | No |
476
498
  | Guard combinators | and/or/not | and/or/not | No | N/A | No |
477
499
  | Effects dual-track | enqueue | enqueueActions | reduce/action | enq.effect() | array of names |
@@ -487,12 +509,23 @@ Example-first, PBT-augmented. Lesson from jssm: "3000+ tests / 100% coverage" tu
487
509
  | Version | Scope |
488
510
  | ------- | ------------------------------------------------------------------ |
489
511
  | v0.1 | core + guards + effects + inspect + replay + pbt (this release) |
490
- | v0.2 | Hierarchical / compound states, including entry/exit ordering |
491
- | v0.3 | Parallel state regions |
492
- | v0.4 | Actor invocation (async) and spawn |
512
+ | v0.2 | Async-guard detection, coverage tuning, llms-full.txt verify gate |
513
+ | v0.3 | Hierarchical sugar via `state.sub` (experimental, this release) |
514
+ | v0.4 | `historyState` — remember last active sub-state on re-entry |
493
515
  | v0.5 | `aifsmjs-bridge-bitecs` / `aifsmjs-bridge-pixi` (separate sub-packages) |
494
516
  | v1.0 | API freeze and stability guarantee |
495
517
 
518
+ **Out of scope (v1)**:
519
+
520
+ - **Parallel state regions** (out of scope for v1)
521
+ - **Actor invocation / spawn** (out of scope for v1)
522
+ - **Tick / game-loop hook** (out of scope for v1)
523
+ - **ECS / Pixi bridges** (out of scope for v1)
524
+
525
+ **v0.4 candidate**: `historyState` — remember last active sub-state on
526
+ re-entry. Workaround today: snapshot via `onTransition` and restore
527
+ manually.
528
+
496
529
  ---
497
530
 
498
531
  ## License
@@ -510,6 +543,57 @@ All notable changes to this project will be documented in this file.
510
543
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
511
544
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
512
545
 
546
+ ## [0.3.0] — 2026-05-29
547
+
548
+ ### Added
549
+
550
+ - **Hierarchical / sub-machine sugar** (experimental). `StateDef` accepts
551
+ two new optional fields: `sub` (a child `SubMachineDef`) and `subImpl`
552
+ (the child's `Implementations`). When the runtime enters a state with
553
+ `sub`, a child `Runtime` is lazily instantiated; when it exits, the child
554
+ is disposed. Access via `runtime.subRuntime()`. See `STABILITY.md` for
555
+ the experimental contract.
556
+ - New error: `SubMachineError` (`{ parentState, phase, cause }`) thrown
557
+ by `send()` / `reset()` on child init / dispose failure. `dispose()`
558
+ cascade swallows child dispose exceptions (idempotent + never-throws
559
+ contract).
560
+ - New type alias: `SubMachineDef<SubCtx, SubEvt, SubStates>`.
561
+ - **`runtime.onTransition(handler, opts?)`** — semantic sugar over
562
+ `runtime.on('transition', handler, opts)`. One-line delegation; shares
563
+ the same listener Set, so registration order across both APIs determines
564
+ invocation order. Returned unsubscribe identical to `on('transition', ...)`.
565
+
566
+ ### Stability
567
+
568
+ - New file: `STABILITY.md`. Documents the three tiers: **stable**
569
+ (everything shipped 0.1.0–0.2.1), **experimental** (`sub`, `subImpl`,
570
+ `subRuntime`, `SubMachineError`, `SubMachineDef`), **draft**
571
+ (`historyState`, v0.4 candidate).
572
+
573
+ ### Changed (positioning)
574
+
575
+ - **README "Capabilities / Limitations" table**: "Hierarchical / compound
576
+ states" moved out of the "Won't do" column. Added on the "Will do" side
577
+ as "Hierarchical sugar via `state.sub` (experimental since 0.3.0)".
578
+ - **README Lifecycle Invariants**: documented the sub-machine ordering —
579
+ parent `step()` lifecycle (exit / actions / entry) runs first, then
580
+ child dispose → child init → snapshot commit → middleware → effects →
581
+ `transition` emit.
582
+
583
+ ### Build & tooling
584
+
585
+ - **`scripts/check-size.mjs`**: core gzip budget raised 3,700 → 4,700 B
586
+ to absorb sub-machine lifecycle, `SubMachineError`, and `onTransition`
587
+ sugar (measured at 4,465 B). `pbt` budget raised 4,600 → 5,500 B because
588
+ `pbt/properties.ts` imports `createRuntime` from `runtime.ts`; the new
589
+ sub-machine code is pulled in transitively (measured at 5,228 B).
590
+
591
+ ### Compatibility
592
+
593
+ This release is **non-breaking** for v0.2.1 callers who do not opt into
594
+ the new sub-machine fields. All existing API signatures, error types, and
595
+ runtime behaviour are byte-identical.
596
+
513
597
  ## [0.2.1] — 2026-05-28
514
598
 
515
599
  ### Security
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aifsmjs",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
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",
@@ -74,6 +74,7 @@
74
74
  "dist",
75
75
  "README.md",
76
76
  "README_ZHTW.md",
77
+ "STABILITY.md",
77
78
  "LICENSE",
78
79
  "llms.txt",
79
80
  "llms-full.txt"