@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.
- package/README.md +207 -1
- package/dist/Machine.d.ts +426 -240
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +219 -43
- package/dist/Machine.js.map +1 -1
- package/dist/internal/machine/activities.d.ts +1 -1
- package/dist/internal/machine/activities.js +1 -1
- package/dist/internal/machine/atom.d.ts +1 -1
- package/dist/internal/machine/atom.js +1 -1
- package/dist/internal/machine/cluster.d.ts +2 -2
- package/dist/internal/machine/cluster.js +1 -1
- package/dist/internal/machine/command.d.ts +1 -1
- package/dist/internal/machine/command.js +1 -1
- package/dist/internal/machine/commandRuntime.d.ts +1 -1
- package/dist/internal/machine/commandRuntime.js +1 -1
- package/dist/internal/machine/configuration.d.ts +1 -1
- package/dist/internal/machine/configuration.js +1 -1
- package/dist/internal/machine/errors.d.ts +7 -7
- package/dist/internal/machine/errors.js +7 -7
- package/dist/internal/machine/executionPlan.d.ts +2 -2
- package/dist/internal/machine/executionPlan.d.ts.map +1 -1
- package/dist/internal/machine/executionPlan.js +17 -8
- package/dist/internal/machine/executionPlan.js.map +1 -1
- package/dist/internal/machine/invocation.d.ts +1 -1
- package/dist/internal/machine/invocation.js +1 -1
- package/dist/internal/machine/planner.d.ts +1 -1
- package/dist/internal/machine/planner.js +1 -1
- package/dist/internal/machine/process.d.ts +1 -1
- package/dist/internal/machine/process.d.ts.map +1 -1
- package/dist/internal/machine/process.js +76 -24
- package/dist/internal/machine/process.js.map +1 -1
- package/dist/internal/machine/protocol.d.ts +1 -1
- package/dist/internal/machine/protocol.js +1 -1
- package/dist/internal/machine/runtime.d.ts +32 -4
- package/dist/internal/machine/runtime.d.ts.map +1 -1
- package/dist/internal/machine/runtime.js +120 -12
- package/dist/internal/machine/runtime.js.map +1 -1
- package/dist/internal/machine/serialization.d.ts +1 -1
- package/dist/internal/machine/serialization.js +1 -1
- package/dist/internal/machine/topology.d.ts +1 -1
- package/dist/internal/machine/topology.js +1 -1
- package/dist/internal/testing/machine/arbitrary.d.ts +3 -3
- package/dist/internal/testing/machine/exploration.d.ts +30 -0
- package/dist/internal/testing/machine/exploration.d.ts.map +1 -0
- package/dist/internal/testing/machine/exploration.js +237 -0
- package/dist/internal/testing/machine/exploration.js.map +1 -0
- package/dist/internal/testing/machine/finiteModel.d.ts +20 -20
- package/dist/internal/testing/machine/finiteModel.js +2 -2
- package/dist/internal/testing/machine/invariant.d.ts +31 -0
- package/dist/internal/testing/machine/invariant.d.ts.map +1 -0
- package/dist/internal/testing/machine/invariant.js +214 -0
- package/dist/internal/testing/machine/invariant.js.map +1 -0
- package/dist/internal/testing/machine/probe.d.ts +26 -0
- package/dist/internal/testing/machine/probe.d.ts.map +1 -0
- package/dist/internal/testing/machine/probe.js +45 -0
- package/dist/internal/testing/machine/probe.js.map +1 -0
- package/dist/internal/testing/machine/referenceModel.d.ts +14 -14
- package/dist/internal/testing/machine/referenceModel.js +2 -2
- package/dist/internal/testing/machine/runtime.d.ts +329 -26
- package/dist/internal/testing/machine/runtime.d.ts.map +1 -1
- package/dist/internal/testing/machine/runtime.js +303 -14
- package/dist/internal/testing/machine/runtime.js.map +1 -1
- package/dist/internal/testing/machine/runtimeInvariant.d.ts +32 -0
- package/dist/internal/testing/machine/runtimeInvariant.d.ts.map +1 -0
- package/dist/internal/testing/machine/runtimeInvariant.js +334 -0
- package/dist/internal/testing/machine/runtimeInvariant.js.map +1 -0
- package/dist/internal/testing/machine/trace.d.ts +17 -0
- package/dist/internal/testing/machine/trace.d.ts.map +1 -0
- package/dist/internal/testing/machine/trace.js +94 -0
- package/dist/internal/testing/machine/trace.js.map +1 -0
- package/dist/internal/testing/machine/verification.d.ts +9 -6
- package/dist/internal/testing/machine/verification.d.ts.map +1 -1
- package/dist/internal/testing/machine/verification.js +8 -79
- package/dist/internal/testing/machine/verification.js.map +1 -1
- package/dist/testing/MachineTest.d.ts +1029 -47
- package/dist/testing/MachineTest.d.ts.map +1 -1
- package/dist/testing/MachineTest.js +315 -11
- package/dist/testing/MachineTest.js.map +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +1 -1
- package/dist/unstable/cluster/ClusterMachine.d.ts +38 -19
- package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -1
- package/dist/unstable/cluster/ClusterMachine.js +27 -9
- package/dist/unstable/cluster/ClusterMachine.js.map +1 -1
- package/dist/unstable/reactivity/AtomMachine.d.ts +82 -31
- package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
- package/dist/unstable/reactivity/AtomMachine.js +63 -12
- package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
- package/docs/agent-guide.md +147 -0
- 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
|
|
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
|