@typeonce/effect-machine 0.4.0 → 0.5.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.
Files changed (90) hide show
  1. package/README.md +207 -1
  2. package/dist/Machine.d.ts +426 -240
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +219 -43
  5. package/dist/Machine.js.map +1 -1
  6. package/dist/internal/machine/activities.d.ts +1 -1
  7. package/dist/internal/machine/activities.js +1 -1
  8. package/dist/internal/machine/atom.d.ts +1 -1
  9. package/dist/internal/machine/atom.js +1 -1
  10. package/dist/internal/machine/cluster.d.ts +2 -2
  11. package/dist/internal/machine/cluster.js +1 -1
  12. package/dist/internal/machine/command.d.ts +1 -1
  13. package/dist/internal/machine/command.js +1 -1
  14. package/dist/internal/machine/commandRuntime.d.ts +1 -1
  15. package/dist/internal/machine/commandRuntime.js +1 -1
  16. package/dist/internal/machine/configuration.d.ts +1 -1
  17. package/dist/internal/machine/configuration.js +1 -1
  18. package/dist/internal/machine/errors.d.ts +7 -7
  19. package/dist/internal/machine/errors.js +7 -7
  20. package/dist/internal/machine/executionPlan.d.ts +2 -2
  21. package/dist/internal/machine/executionPlan.d.ts.map +1 -1
  22. package/dist/internal/machine/executionPlan.js +17 -8
  23. package/dist/internal/machine/executionPlan.js.map +1 -1
  24. package/dist/internal/machine/invocation.d.ts +1 -1
  25. package/dist/internal/machine/invocation.js +1 -1
  26. package/dist/internal/machine/planner.d.ts +1 -1
  27. package/dist/internal/machine/planner.js +1 -1
  28. package/dist/internal/machine/process.d.ts +1 -1
  29. package/dist/internal/machine/process.d.ts.map +1 -1
  30. package/dist/internal/machine/process.js +76 -24
  31. package/dist/internal/machine/process.js.map +1 -1
  32. package/dist/internal/machine/protocol.d.ts +1 -1
  33. package/dist/internal/machine/protocol.js +1 -1
  34. package/dist/internal/machine/runtime.d.ts +32 -4
  35. package/dist/internal/machine/runtime.d.ts.map +1 -1
  36. package/dist/internal/machine/runtime.js +120 -12
  37. package/dist/internal/machine/runtime.js.map +1 -1
  38. package/dist/internal/machine/serialization.d.ts +1 -1
  39. package/dist/internal/machine/serialization.js +1 -1
  40. package/dist/internal/machine/topology.d.ts +1 -1
  41. package/dist/internal/machine/topology.js +1 -1
  42. package/dist/internal/testing/machine/arbitrary.d.ts +3 -3
  43. package/dist/internal/testing/machine/exploration.d.ts +30 -0
  44. package/dist/internal/testing/machine/exploration.d.ts.map +1 -0
  45. package/dist/internal/testing/machine/exploration.js +237 -0
  46. package/dist/internal/testing/machine/exploration.js.map +1 -0
  47. package/dist/internal/testing/machine/finiteModel.d.ts +20 -20
  48. package/dist/internal/testing/machine/finiteModel.js +2 -2
  49. package/dist/internal/testing/machine/invariant.d.ts +31 -0
  50. package/dist/internal/testing/machine/invariant.d.ts.map +1 -0
  51. package/dist/internal/testing/machine/invariant.js +214 -0
  52. package/dist/internal/testing/machine/invariant.js.map +1 -0
  53. package/dist/internal/testing/machine/probe.d.ts +26 -0
  54. package/dist/internal/testing/machine/probe.d.ts.map +1 -0
  55. package/dist/internal/testing/machine/probe.js +45 -0
  56. package/dist/internal/testing/machine/probe.js.map +1 -0
  57. package/dist/internal/testing/machine/referenceModel.d.ts +14 -14
  58. package/dist/internal/testing/machine/referenceModel.js +2 -2
  59. package/dist/internal/testing/machine/runtime.d.ts +329 -26
  60. package/dist/internal/testing/machine/runtime.d.ts.map +1 -1
  61. package/dist/internal/testing/machine/runtime.js +303 -14
  62. package/dist/internal/testing/machine/runtime.js.map +1 -1
  63. package/dist/internal/testing/machine/runtimeInvariant.d.ts +32 -0
  64. package/dist/internal/testing/machine/runtimeInvariant.d.ts.map +1 -0
  65. package/dist/internal/testing/machine/runtimeInvariant.js +334 -0
  66. package/dist/internal/testing/machine/runtimeInvariant.js.map +1 -0
  67. package/dist/internal/testing/machine/trace.d.ts +17 -0
  68. package/dist/internal/testing/machine/trace.d.ts.map +1 -0
  69. package/dist/internal/testing/machine/trace.js +94 -0
  70. package/dist/internal/testing/machine/trace.js.map +1 -0
  71. package/dist/internal/testing/machine/verification.d.ts +9 -6
  72. package/dist/internal/testing/machine/verification.d.ts.map +1 -1
  73. package/dist/internal/testing/machine/verification.js +8 -79
  74. package/dist/internal/testing/machine/verification.js.map +1 -1
  75. package/dist/testing/MachineTest.d.ts +1029 -47
  76. package/dist/testing/MachineTest.d.ts.map +1 -1
  77. package/dist/testing/MachineTest.js +315 -11
  78. package/dist/testing/MachineTest.js.map +1 -1
  79. package/dist/testing/index.d.ts +1 -1
  80. package/dist/testing/index.js +1 -1
  81. package/dist/unstable/cluster/ClusterMachine.d.ts +38 -19
  82. package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -1
  83. package/dist/unstable/cluster/ClusterMachine.js +27 -9
  84. package/dist/unstable/cluster/ClusterMachine.js.map +1 -1
  85. package/dist/unstable/reactivity/AtomMachine.d.ts +82 -31
  86. package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
  87. package/dist/unstable/reactivity/AtomMachine.js +63 -12
  88. package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
  89. package/docs/agent-guide.md +147 -0
  90. package/package.json +11 -2
package/README.md CHANGED
@@ -21,10 +21,11 @@ require upgrading Effect in lockstep; do not override the peer to another beta.
21
21
  import { Machine } from "@typeonce/effect-machine"
22
22
  import { ClusterMachine } from "@typeonce/effect-machine/cluster"
23
23
  import { AtomMachine } from "@typeonce/effect-machine/reactivity"
24
+ import { MachineTest } from "@typeonce/effect-machine/testing"
24
25
  ```
25
26
 
26
27
  Each ESM entrypoint is independent and tree-shakeable. Importing the root does
27
- not load the reactivity or cluster adapters.
28
+ not load the reactivity, cluster, or testing modules.
28
29
 
29
30
  ## First machine
30
31
 
@@ -655,6 +656,211 @@ restrictions, checkpoint planning, and delivery guarantees are documented on
655
656
  that API. `Machine.resume` is logical resumption, not durable process or cluster
656
657
  restoration.
657
658
 
659
+ ## Property-based semantic invariants
660
+
661
+ `MachineTest.verify` checks statechart structure and planner lifecycle laws.
662
+ Application semantics belong in invariants that can be reused across generated
663
+ scenarios and, in future, bounded exploration:
664
+
665
+ ```ts
666
+ import { MachineTest } from "@typeonce/effect-machine/testing"
667
+ import { Effect } from "effect"
668
+
669
+ const invariant = MachineTest.invariants(accountMachine)
670
+ const laws = [
671
+ invariant.state(
672
+ "balance is never negative",
673
+ ({ snapshot }) =>
674
+ snapshot.value.balance >= 0 ||
675
+ `negative balance: ${snapshot.value.balance}`
676
+ ),
677
+ invariant.step(
678
+ "withdrawal removes exactly its amount",
679
+ ({ before, event, after }) =>
680
+ event._tag !== "Withdraw" ||
681
+ after.value.balance === before.value.balance - event.amount
682
+ )
683
+ ]
684
+
685
+ const generated = MachineTest.scenarios(accountMachine, {
686
+ minEvents: 0,
687
+ maxEvents: 30
688
+ })
689
+
690
+ it.effect.prop(
691
+ "preserves account laws",
692
+ { scenario: generated.arbitrary },
693
+ ({ scenario }) =>
694
+ MachineTest.run(accountMachine, scenario).pipe(
695
+ Effect.tap((trace) => MachineTest.verify(accountMachine, trace)),
696
+ Effect.flatMap((trace) => MachineTest.assertInvariants(accountMachine, trace, laws))
697
+ )
698
+ )
699
+ ```
700
+
701
+ State invariants observe settled startup and public-event states by default.
702
+ Set `observe` to `"microsteps"`, `"all"`, or `"final"` for a different scope.
703
+ Use `when` for conditional laws. A condition with no matches is reported as
704
+ `untested`; add `require: { minObservations: 1 }` when a particular trace must
705
+ exercise it. `checkInvariants` returns this report, while `assertInvariants`
706
+ returns `void` for direct use in property tests. Failures retain the complete
707
+ shrunk trace and precise event, microstep, configuration, and observation
708
+ location.
709
+
710
+ These APIs inspect planner evidence. Staged action effects, invokes, timing,
711
+ and process scheduling require the runtime command-model APIs instead.
712
+
713
+ Use bounded exploration when random scenarios should be complemented by a
714
+ systematic search over concrete event representatives:
715
+
716
+ ```ts
717
+ const explored = yield * MachineTest.explore(accountMachine, {
718
+ events: ({ snapshot }) => [
719
+ new Deposit({ amount: 1 }),
720
+ new Withdraw({ amount: snapshot.value.balance }),
721
+ new Withdraw({ amount: snapshot.value.balance + 1 })
722
+ ],
723
+ stateKey: ({ snapshot }) => `${snapshot.value._tag}:${snapshot.value.balance}`,
724
+ limits: {
725
+ maxDepth: 20,
726
+ maxStates: 1_000,
727
+ maxTransitions: 10_000
728
+ },
729
+ invariants: laws
730
+ })
731
+
732
+ const rejected = yield * MachineTest.assertReachable(
733
+ explored,
734
+ "insufficient funds rejection",
735
+ ({ configuration }) => configuration.includes("Rejected")
736
+ )
737
+
738
+ console.log(rejected.trace.scenario.events) // shortest witness
739
+ ```
740
+
741
+ Exploration is breadth-first, so each retained node owns its shortest trace.
742
+ It is exhaustive only for the concrete events returned by `events` and the
743
+ equivalence relation defined by `stateKey`. Equal keys intentionally collapse
744
+ snapshots and only the first representative is expanded. Results distinguish
745
+ `Complete` from `Truncated` and retain the depth, state, or transition frontier
746
+ that hit a limit. An unreachability assertion succeeds only for a complete
747
+ result; otherwise it fails as inconclusive. Cycles are retained as graph edges,
748
+ but exploration does not enumerate every cyclic path. Invariants are checked
749
+ on startup and on each planned edge extending a node's shortest trace.
750
+
751
+ ## Causal runtime probes
752
+
753
+ Pure traces do not execute invokes or the managed runtime. When a test needs to
754
+ prove that one live event has actually left the mailbox, attach a testing-only
755
+ probe to a statechart reference:
756
+
757
+ ```ts
758
+ const ref = yield * Machine.start(machine)
759
+ const probe = yield * MachineTest.probe(machine, ref)
760
+
761
+ const step = yield * probe.sendAndAwait(new CancelRequested({}))
762
+
763
+ assert.strictEqual(step.handled, false)
764
+ assert.deepStrictEqual(step.before, step.after)
765
+ ```
766
+
767
+ `sendAndAwait` completes after that event's synchronous macrostep and managed
768
+ commit work. It also completes for ignored events, which publish no snapshot
769
+ and therefore cannot be synchronized by waiting for `ref.changes`.
770
+
771
+ The step retains the exact runtime plan, before/after logical snapshots, and
772
+ whether the event was handled or changed/reentered the active configuration.
773
+ It does not wait for timers or invoked processes to finish. Production code
774
+ continues to use enqueue-only `ref.send`; probes are exported only from the
775
+ separate testing entry point.
776
+
777
+ For command-model and property tests, choose the delivery semantics explicitly.
778
+ `runCausalCommands` requires a probe and completes every accepted send before
779
+ checking it or starting the next command:
780
+
781
+ ```ts
782
+ const transcript = yield * MachineTest.runCausalCommands(
783
+ probe,
784
+ commands,
785
+ {
786
+ initialModel,
787
+ transition: (model, command) =>
788
+ Effect.succeed({
789
+ model: updateModel(model, command),
790
+ expected: expectedResult(model, command)
791
+ }),
792
+ assert: ({ actual, expected }) =>
793
+ Effect.sync(() => {
794
+ if (actual.result._tag === "SendProcessed") {
795
+ assert.deepStrictEqual(actual.result.step.after, expected.snapshot)
796
+ assert.strictEqual(actual.result.step.handled, expected.handled)
797
+ }
798
+ })
799
+ }
800
+ )
801
+ ```
802
+
803
+ A causal model step needs no synchronization policy. Use the probe-bound
804
+ `probe.await.until(...)` only for later asynchronous work such as a timer,
805
+ invoke result, or child delivery. The predicate sees the exact runtime snapshot
806
+ type. `actual.awaited` retains every snapshot tested by that explicit wait.
807
+
808
+ Use `runEnqueuedCommands(ref, ...)` when the property intentionally submits
809
+ bursts or retains outstanding mailbox work. Its model steps continue to use
810
+ `RuntimeSynchronization`. The old `runRuntimeCommands` and
811
+ `formatRuntimeTranscript` names are deprecated aliases for the enqueue-oriented
812
+ runner and formatter because their delivery semantics were not visible.
813
+
814
+ ### Runtime invariants and planner agreement
815
+
816
+ Planner invariants and runtime invariants are deliberately separate. Runtime
817
+ laws inspect causal command evidence, explicit asynchronous observations, and
818
+ runtime status without requiring a duplicate reference model:
819
+
820
+ ```ts
821
+ const invariant = MachineTest.runtimeInvariants(machine)
822
+ const laws = [
823
+ invariant.snapshot("count never becomes negative", ({ snapshot }) => snapshot.state.value.count >= 0),
824
+ invariant.command(
825
+ "every accepted add is processed",
826
+ ({ command, result }) =>
827
+ command._tag !== "Send" || command.event._tag !== "Add" ||
828
+ result._tag === "SendProcessed"
829
+ )
830
+ ]
831
+
832
+ const transcript = yield * MachineTest.verifyCausalCommands(
833
+ probe,
834
+ commands,
835
+ { invariants: laws }
836
+ )
837
+ ```
838
+
839
+ Use the existing `runCausalCommands` when a simplified application model
840
+ provides exact expected results. Its returned transcript implements the same
841
+ model-independent evidence interface, so reusable runtime laws compose with
842
+ it directly:
843
+
844
+ ```ts
845
+ const transcript = yield * MachineTest.runCausalCommands(probe, commands, model)
846
+
847
+ yield * MachineTest.assertRuntimeInvariants(machine, transcript, laws)
848
+ yield * MachineTest.assertPlannerRuntimeAgreement(machine, transcript)
849
+ ```
850
+
851
+ `checkRuntimeInvariants` returns an aggregate report; `assertRuntimeInvariants`
852
+ fails with every predicate and non-vacuity violation. Snapshot laws observe the
853
+ initial and post-command snapshots by default. Select `"awaited"`, `"all"`, or
854
+ `"final"` explicitly when a law targets observations retained by
855
+ `probe.await.until` or only the final runtime snapshot.
856
+
857
+ `assertPlannerRuntimeAgreement` is an explicit consistency check, not an
858
+ application oracle. For each processed send it freshly plans from the receipt's
859
+ `before` snapshot and compares handled/change flags, the public next snapshots,
860
+ completion, command counts, emitted events, and public microstep evidence. It
861
+ does not prove that the planner implements the intended business rules; use a
862
+ reference model and runtime invariants for that.
863
+
658
864
  ## Current limits
659
865
 
660
866
  Declarative first-class guards are not part of the current API. Ordinary