@typeonce/effect-machine 0.3.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 (170) hide show
  1. package/README.md +410 -17
  2. package/dist/Machine.d.ts +1191 -510
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +371 -565
  5. package/dist/Machine.js.map +1 -1
  6. package/dist/internal/machine/activities.d.ts +57 -0
  7. package/dist/internal/machine/activities.d.ts.map +1 -0
  8. package/dist/internal/machine/activities.js +49 -0
  9. package/dist/internal/machine/activities.js.map +1 -0
  10. package/dist/internal/machine/atom.d.ts +69 -0
  11. package/dist/internal/machine/atom.d.ts.map +1 -0
  12. package/dist/{AtomMachine.js → internal/machine/atom.js} +20 -95
  13. package/dist/internal/machine/atom.js.map +1 -0
  14. package/dist/internal/machine/cluster.d.ts +63 -0
  15. package/dist/internal/machine/cluster.d.ts.map +1 -0
  16. package/dist/{ClusterMachine.js → internal/machine/cluster.js} +15 -119
  17. package/dist/internal/machine/cluster.js.map +1 -0
  18. package/dist/internal/machine/command.d.ts +16 -0
  19. package/dist/internal/machine/command.d.ts.map +1 -0
  20. package/dist/internal/machine/command.js +45 -0
  21. package/dist/internal/machine/command.js.map +1 -0
  22. package/dist/internal/machine/commandRuntime.d.ts +13 -0
  23. package/dist/internal/machine/commandRuntime.d.ts.map +1 -0
  24. package/dist/internal/machine/commandRuntime.js +16 -0
  25. package/dist/internal/machine/commandRuntime.js.map +1 -0
  26. package/dist/internal/{machineModel.d.ts → machine/configuration.d.ts} +12 -61
  27. package/dist/internal/machine/configuration.d.ts.map +1 -0
  28. package/dist/internal/{machineModel.js → machine/configuration.js} +250 -545
  29. package/dist/internal/machine/configuration.js.map +1 -0
  30. package/dist/internal/{machineErrors.d.ts → machine/errors.d.ts} +8 -8
  31. package/dist/internal/machine/errors.d.ts.map +1 -0
  32. package/dist/internal/{machineErrors.js → machine/errors.js} +8 -8
  33. package/dist/internal/machine/errors.js.map +1 -0
  34. package/dist/internal/machine/executionPlan.d.ts +49 -0
  35. package/dist/internal/machine/executionPlan.d.ts.map +1 -0
  36. package/dist/internal/machine/executionPlan.js +676 -0
  37. package/dist/internal/machine/executionPlan.js.map +1 -0
  38. package/dist/internal/machine/invocation.d.ts +23 -0
  39. package/dist/internal/machine/invocation.d.ts.map +1 -0
  40. package/dist/internal/machine/invocation.js +77 -0
  41. package/dist/internal/machine/invocation.js.map +1 -0
  42. package/dist/internal/machine/machine.d.ts +183 -0
  43. package/dist/internal/machine/machine.d.ts.map +1 -0
  44. package/dist/internal/machine/machine.js +552 -0
  45. package/dist/internal/machine/machine.js.map +1 -0
  46. package/dist/internal/machine/planner.d.ts +182 -0
  47. package/dist/internal/machine/planner.d.ts.map +1 -0
  48. package/dist/internal/machine/planner.js +1082 -0
  49. package/dist/internal/machine/planner.js.map +1 -0
  50. package/dist/internal/{machineProcess.d.ts → machine/process.d.ts} +11 -5
  51. package/dist/internal/machine/process.d.ts.map +1 -0
  52. package/dist/internal/machine/process.js +498 -0
  53. package/dist/internal/machine/process.js.map +1 -0
  54. package/dist/internal/machine/protocol.d.ts +34 -0
  55. package/dist/internal/machine/protocol.d.ts.map +1 -0
  56. package/dist/internal/machine/protocol.js +182 -0
  57. package/dist/internal/machine/protocol.js.map +1 -0
  58. package/dist/internal/machine/readiness.d.ts +4 -0
  59. package/dist/internal/machine/readiness.d.ts.map +1 -0
  60. package/dist/internal/machine/readiness.js +2 -0
  61. package/dist/internal/machine/readiness.js.map +1 -0
  62. package/dist/internal/machine/runtime.d.ts +255 -0
  63. package/dist/internal/machine/runtime.d.ts.map +1 -0
  64. package/dist/internal/machine/runtime.js +1489 -0
  65. package/dist/internal/machine/runtime.js.map +1 -0
  66. package/dist/internal/machine/serialization.d.ts +14 -0
  67. package/dist/internal/machine/serialization.d.ts.map +1 -0
  68. package/dist/internal/machine/serialization.js +338 -0
  69. package/dist/internal/machine/serialization.js.map +1 -0
  70. package/dist/internal/machine/stateDefinition.d.ts +15 -0
  71. package/dist/internal/machine/stateDefinition.d.ts.map +1 -0
  72. package/dist/internal/machine/stateDefinition.js +210 -0
  73. package/dist/internal/machine/stateDefinition.js.map +1 -0
  74. package/dist/internal/machine/symbols.d.ts +3 -0
  75. package/dist/internal/machine/symbols.d.ts.map +1 -0
  76. package/dist/internal/machine/symbols.js +3 -0
  77. package/dist/internal/machine/symbols.js.map +1 -0
  78. package/dist/internal/machine/topology.d.ts +98 -0
  79. package/dist/internal/machine/topology.d.ts.map +1 -0
  80. package/dist/internal/machine/topology.js +347 -0
  81. package/dist/internal/machine/topology.js.map +1 -0
  82. package/dist/internal/testing/machine/arbitrary.d.ts +36 -0
  83. package/dist/internal/testing/machine/arbitrary.d.ts.map +1 -0
  84. package/dist/internal/testing/machine/arbitrary.js +68 -0
  85. package/dist/internal/testing/machine/arbitrary.js.map +1 -0
  86. package/dist/internal/testing/machine/exploration.d.ts +30 -0
  87. package/dist/internal/testing/machine/exploration.d.ts.map +1 -0
  88. package/dist/internal/testing/machine/exploration.js +237 -0
  89. package/dist/internal/testing/machine/exploration.js.map +1 -0
  90. package/dist/internal/testing/machine/finiteModel.d.ts +322 -0
  91. package/dist/internal/testing/machine/finiteModel.d.ts.map +1 -0
  92. package/dist/internal/testing/machine/finiteModel.js +976 -0
  93. package/dist/internal/testing/machine/finiteModel.js.map +1 -0
  94. package/dist/internal/testing/machine/invariant.d.ts +31 -0
  95. package/dist/internal/testing/machine/invariant.d.ts.map +1 -0
  96. package/dist/internal/testing/machine/invariant.js +214 -0
  97. package/dist/internal/testing/machine/invariant.js.map +1 -0
  98. package/dist/internal/testing/machine/probe.d.ts +26 -0
  99. package/dist/internal/testing/machine/probe.d.ts.map +1 -0
  100. package/dist/internal/testing/machine/probe.js +45 -0
  101. package/dist/internal/testing/machine/probe.js.map +1 -0
  102. package/dist/internal/testing/machine/referenceModel.d.ts +203 -0
  103. package/dist/internal/testing/machine/referenceModel.d.ts.map +1 -0
  104. package/dist/internal/testing/machine/referenceModel.js +1012 -0
  105. package/dist/internal/testing/machine/referenceModel.js.map +1 -0
  106. package/dist/internal/testing/machine/runtime.d.ts +631 -0
  107. package/dist/internal/testing/machine/runtime.d.ts.map +1 -0
  108. package/dist/internal/testing/machine/runtime.js +726 -0
  109. package/dist/internal/testing/machine/runtime.js.map +1 -0
  110. package/dist/internal/testing/machine/runtimeInvariant.d.ts +32 -0
  111. package/dist/internal/testing/machine/runtimeInvariant.d.ts.map +1 -0
  112. package/dist/internal/testing/machine/runtimeInvariant.js +334 -0
  113. package/dist/internal/testing/machine/runtimeInvariant.js.map +1 -0
  114. package/dist/internal/testing/machine/trace.d.ts +17 -0
  115. package/dist/internal/testing/machine/trace.d.ts.map +1 -0
  116. package/dist/internal/testing/machine/trace.js +94 -0
  117. package/dist/internal/testing/machine/trace.js.map +1 -0
  118. package/dist/internal/testing/machine/verification.d.ts +36 -0
  119. package/dist/internal/testing/machine/verification.d.ts.map +1 -0
  120. package/dist/internal/testing/machine/verification.js +1375 -0
  121. package/dist/internal/testing/machine/verification.js.map +1 -0
  122. package/dist/testing/MachineTest.d.ts +1588 -0
  123. package/dist/testing/MachineTest.d.ts.map +1 -0
  124. package/dist/testing/MachineTest.js +406 -0
  125. package/dist/testing/MachineTest.js.map +1 -0
  126. package/dist/testing/index.d.ts +7 -0
  127. package/dist/testing/index.d.ts.map +1 -0
  128. package/dist/testing/index.js +7 -0
  129. package/dist/testing/index.js.map +1 -0
  130. package/dist/{ClusterMachine.d.ts → unstable/cluster/ClusterMachine.d.ts} +56 -45
  131. package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -0
  132. package/dist/unstable/cluster/ClusterMachine.js +144 -0
  133. package/dist/unstable/cluster/ClusterMachine.js.map +1 -0
  134. package/dist/{cluster.js → unstable/cluster/index.d.ts} +1 -1
  135. package/dist/unstable/cluster/index.d.ts.map +1 -0
  136. package/dist/{cluster.d.ts → unstable/cluster/index.js} +1 -1
  137. package/dist/unstable/cluster/index.js.map +1 -0
  138. package/dist/{AtomMachine.d.ts → unstable/reactivity/AtomMachine.d.ts} +116 -50
  139. package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -0
  140. package/dist/unstable/reactivity/AtomMachine.js +164 -0
  141. package/dist/unstable/reactivity/AtomMachine.js.map +1 -0
  142. package/dist/{reactivity.js → unstable/reactivity/index.d.ts} +1 -1
  143. package/dist/unstable/reactivity/index.d.ts.map +1 -0
  144. package/dist/{reactivity.d.ts → unstable/reactivity/index.js} +1 -1
  145. package/dist/unstable/reactivity/index.js.map +1 -0
  146. package/docs/agent-guide.md +316 -31
  147. package/package.json +28 -10
  148. package/dist/AtomMachine.d.ts.map +0 -1
  149. package/dist/AtomMachine.js.map +0 -1
  150. package/dist/ClusterMachine.d.ts.map +0 -1
  151. package/dist/ClusterMachine.js.map +0 -1
  152. package/dist/cluster.d.ts.map +0 -1
  153. package/dist/cluster.js.map +0 -1
  154. package/dist/internal/machineErrors.d.ts.map +0 -1
  155. package/dist/internal/machineErrors.js.map +0 -1
  156. package/dist/internal/machineModel.d.ts.map +0 -1
  157. package/dist/internal/machineModel.js.map +0 -1
  158. package/dist/internal/machinePlanner.d.ts +0 -74
  159. package/dist/internal/machinePlanner.d.ts.map +0 -1
  160. package/dist/internal/machinePlanner.js +0 -797
  161. package/dist/internal/machinePlanner.js.map +0 -1
  162. package/dist/internal/machineProcess.d.ts.map +0 -1
  163. package/dist/internal/machineProcess.js +0 -181
  164. package/dist/internal/machineProcess.js.map +0 -1
  165. package/dist/internal/machineRuntime.d.ts +0 -125
  166. package/dist/internal/machineRuntime.d.ts.map +0 -1
  167. package/dist/internal/machineRuntime.js +0 -365
  168. package/dist/internal/machineRuntime.js.map +0 -1
  169. package/dist/reactivity.d.ts.map +0 -1
  170. package/dist/reactivity.js.map +0 -1
@@ -0,0 +1,976 @@
1
+ /**
2
+ * Finite hierarchical, parallel, history, choice, and automatic-transition
3
+ * statechart models used by the public testing module.
4
+ *
5
+ * This module intentionally compiles through the public Machine API. It must
6
+ * not share planner helpers with the implementation that later reference
7
+ * models are expected to check.
8
+ *
9
+ * @internal
10
+ */
11
+ import * as Schema from "effect/Schema";
12
+ import { FastCheck } from "effect/testing";
13
+ import * as Machine from "../../../Machine.js";
14
+ const triggerKey = (trigger) => trigger.type === "event" ? `event:${trigger.event}` : trigger.type;
15
+ const defaults = {
16
+ maxRoots: 3,
17
+ maxDepth: 3,
18
+ maxChildren: 3,
19
+ maxParallelRegions: 3,
20
+ maxEvents: 3,
21
+ maxTransitions: 12,
22
+ maxHistoryStates: 2,
23
+ maxChoiceStates: 1
24
+ };
25
+ const validateLimit = (name, value, maximum) => {
26
+ if (!Number.isSafeInteger(value) || value < 1 || value > maximum) {
27
+ throw new Error(`MachineTest.finiteModels expected ${name} to be an integer between 1 and ${maximum}`);
28
+ }
29
+ };
30
+ const resolveOptions = (options) => {
31
+ const limits = {
32
+ maxRoots: options.maxRoots ?? defaults.maxRoots,
33
+ maxDepth: options.maxDepth ?? defaults.maxDepth,
34
+ maxChildren: options.maxChildren ?? defaults.maxChildren,
35
+ maxParallelRegions: options.maxParallelRegions ?? defaults.maxParallelRegions,
36
+ maxEvents: options.maxEvents ?? defaults.maxEvents,
37
+ maxTransitions: options.maxTransitions ?? defaults.maxTransitions,
38
+ maxHistoryStates: options.maxHistoryStates ?? defaults.maxHistoryStates,
39
+ maxChoiceStates: options.maxChoiceStates ?? defaults.maxChoiceStates
40
+ };
41
+ validateLimit("maxRoots", limits.maxRoots, 3);
42
+ validateLimit("maxDepth", limits.maxDepth, 6);
43
+ validateLimit("maxChildren", limits.maxChildren, 4);
44
+ validateLimit("maxParallelRegions", limits.maxParallelRegions, 3);
45
+ if (limits.maxParallelRegions < 2) {
46
+ throw new Error("MachineTest.finiteModels expected maxParallelRegions to be two or three");
47
+ }
48
+ validateLimit("maxEvents", limits.maxEvents, 8);
49
+ validateLimit("maxTransitions", limits.maxTransitions, 256);
50
+ if (!Number.isSafeInteger(limits.maxHistoryStates) || limits.maxHistoryStates < 0 || limits.maxHistoryStates > 8) {
51
+ throw new Error("MachineTest.finiteModels expected maxHistoryStates to be an integer between 0 and 8");
52
+ }
53
+ if (!Number.isSafeInteger(limits.maxChoiceStates) || limits.maxChoiceStates < 0 || limits.maxChoiceStates > 8) {
54
+ throw new Error("MachineTest.finiteModels expected maxChoiceStates to be an integer between 0 and 8");
55
+ }
56
+ return limits;
57
+ };
58
+ const rawStateArbitrary = (depth, limits) => {
59
+ const leaf = FastCheck.boolean().map((final) => final ? { _tag: "Final" } : { _tag: "Atomic" });
60
+ if (depth >= limits.maxDepth)
61
+ return leaf;
62
+ const nested = rawStateArbitrary(depth + 1, limits);
63
+ const compound = FastCheck.array(nested, {
64
+ minLength: 1,
65
+ maxLength: limits.maxChildren
66
+ }).chain((states) => FastCheck.integer({ min: 0, max: states.length - 1 }).map((initialIndex) => ({
67
+ _tag: "Compound",
68
+ initialIndex,
69
+ states
70
+ })));
71
+ const parallel = FastCheck.array(nested, {
72
+ minLength: 2,
73
+ maxLength: limits.maxParallelRegions
74
+ }).map((states) => ({ _tag: "Parallel", states }));
75
+ return FastCheck.oneof(leaf, compound, parallel);
76
+ };
77
+ const normalizeStates = (raw) => {
78
+ let value = 0;
79
+ const visit = (states, parentPath) => states.map((state, index) => {
80
+ const key = `state${index}`;
81
+ const path = parentPath === "" ? key : `${parentPath}.${key}`;
82
+ const currentValue = value++;
83
+ if (state._tag === "Atomic") {
84
+ return Object.freeze({ _tag: "Atomic", key, value: currentValue });
85
+ }
86
+ if (state._tag === "Final") {
87
+ return Object.freeze({
88
+ _tag: "Final",
89
+ key,
90
+ value: currentValue,
91
+ output: `output:${path}`
92
+ });
93
+ }
94
+ const children = visit(state.states, path);
95
+ if (state._tag === "Parallel") {
96
+ return Object.freeze({
97
+ _tag: "Parallel",
98
+ key,
99
+ value: currentValue,
100
+ output: `output:${path}`,
101
+ states: Object.freeze(children.slice())
102
+ });
103
+ }
104
+ return Object.freeze({
105
+ _tag: "Compound",
106
+ key,
107
+ value: currentValue,
108
+ initial: children[state.initialIndex].key,
109
+ states: Object.freeze(children.slice())
110
+ });
111
+ });
112
+ return Object.freeze(visit(raw, "").slice());
113
+ };
114
+ const flattenStates = (roots) => {
115
+ const flattened = [];
116
+ const visit = (states, parent, root) => {
117
+ for (const node of states) {
118
+ const path = parent === undefined ? node.key : `${parent}.${node.key}`;
119
+ const nodeRoot = root ?? path;
120
+ flattened.push({ node, path, parent, root: nodeRoot });
121
+ if (node._tag === "Compound" || node._tag === "Parallel")
122
+ visit(node.states, path, nodeRoot);
123
+ }
124
+ };
125
+ visit(roots, undefined, undefined);
126
+ return flattened;
127
+ };
128
+ const isPathInSubtree = (path, root) => path === root || path.startsWith(`${root}.`);
129
+ const initialLeaves = (states, path) => {
130
+ const state = states.get(path);
131
+ if (state.node._tag === "Choice")
132
+ return initialLeaves(states, state.node.selected);
133
+ if (state.node._tag === "Compound") {
134
+ return initialLeaves(states, `${path}.${state.node.initial}`);
135
+ }
136
+ if (state.node._tag === "Parallel") {
137
+ return state.node.states
138
+ .filter((child) => child._tag !== "History" && child._tag !== "Choice")
139
+ .flatMap((child) => initialLeaves(states, `${path}.${child.key}`));
140
+ }
141
+ return state.node._tag === "Atomic" ? [path] : [];
142
+ };
143
+ const addGeneratedHistory = (roots, initial, maxHistoryStates, maxTransitions, events, decisions) => {
144
+ if (maxHistoryStates === 0 || maxTransitions < 2 || decisions.length === 0) {
145
+ return { roots, scenarios: [] };
146
+ }
147
+ const flattened = flattenStates(roots);
148
+ const byPath = new Map(flattened.map((state) => [state.path, state]));
149
+ const initialActiveLeaves = initialLeaves(byPath, initial);
150
+ const initialActive = new Set(initialActiveLeaves.flatMap((leaf) => {
151
+ const parts = leaf.split(".");
152
+ return parts.map((_, index) => parts.slice(0, index + 1).join("."));
153
+ }));
154
+ const outside = roots
155
+ .filter((root) => root.key !== initial)
156
+ .map((root) => ({ root, leaves: initialLeaves(byPath, root.key) }))
157
+ .find(({ leaves }) => leaves.length > 0);
158
+ if (outside === undefined)
159
+ return { roots, scenarios: [] };
160
+ const eligible = flattened.filter((state) => initialActive.has(state.path) &&
161
+ (state.node._tag === "Compound" || state.node._tag === "Parallel") &&
162
+ initialLeaves(byPath, state.path).length > 0);
163
+ const count = Math.min(maxHistoryStates, decisions.length, Math.floor(events.length / 2), Math.floor(maxTransitions / 3), eligible.length);
164
+ const selected = eligible.slice(0, count).map((owner, index) => {
165
+ const composite = owner.node;
166
+ const directAtomic = composite._tag === "Compound"
167
+ ? composite.states.find((child) => child.key === composite.initial && child._tag === "Atomic")
168
+ : composite.states.find((child) => child._tag === "Atomic");
169
+ const historyType = decisions[index] === "shallow" && directAtomic !== undefined ? "shallow" : "deep";
170
+ const mutationSource = historyType === "shallow"
171
+ ? `${owner.path}.${directAtomic.key}`
172
+ : initialLeaves(byPath, owner.path)[0];
173
+ const fallbackChild = composite._tag === "Compound"
174
+ ? composite.states.find((child) => child.key === composite.initial)
175
+ : composite.states.find((child) => child._tag !== "History");
176
+ const history = Object.freeze({
177
+ _tag: "History",
178
+ key: `history${index}`,
179
+ history: historyType,
180
+ fallback: `${owner.path}.${fallbackChild.key}`
181
+ });
182
+ return { owner, history, mutationSource };
183
+ });
184
+ const byOwner = new Map(selected.map(({ history, owner }) => [owner.path, history]));
185
+ const decorate = (states, parent) => Object.freeze(states.map((state) => {
186
+ const path = parent === undefined ? state.key : `${parent}.${state.key}`;
187
+ if (state._tag !== "Compound" && state._tag !== "Parallel")
188
+ return state;
189
+ const children = decorate(state.states, path);
190
+ const history = byOwner.get(path);
191
+ return Object.freeze({
192
+ ...state,
193
+ states: Object.freeze(history === undefined ? children.slice() : [...children, history])
194
+ });
195
+ }));
196
+ const decorated = decorate(roots, undefined);
197
+ return {
198
+ roots: decorated,
199
+ scenarios: selected.map(({ history, mutationSource, owner }, index) => {
200
+ const historyPath = `${owner.path}.${history.key}`;
201
+ const leaveEvent = events[index];
202
+ const mutationEvent = events[count + index];
203
+ const mutationValue = byPath.get(mutationSource).node.value + 10_000 + index;
204
+ return Object.freeze({
205
+ history: historyPath,
206
+ owner: owner.path,
207
+ historyType: history.history,
208
+ mutation: Object.freeze({
209
+ source: mutationSource,
210
+ event: mutationEvent,
211
+ target: mutationSource,
212
+ value: mutationValue
213
+ }),
214
+ leave: Object.freeze({
215
+ source: initialLeaves(byPath, owner.path)[0],
216
+ event: leaveEvent,
217
+ target: outside.root.key
218
+ }),
219
+ resume: Object.freeze({
220
+ source: outside.leaves[0],
221
+ event: leaveEvent,
222
+ target: historyPath
223
+ }),
224
+ events: Object.freeze([mutationEvent, leaveEvent, leaveEvent])
225
+ });
226
+ })
227
+ };
228
+ };
229
+ const addGeneratedChoices = (roots, maxChoiceStates) => {
230
+ if (maxChoiceStates === 0)
231
+ return roots;
232
+ let remaining = maxChoiceStates;
233
+ const visit = (states, parent, insideParallel) => Object.freeze(states.map((state) => {
234
+ const path = parent === undefined ? state.key : `${parent}.${state.key}`;
235
+ if (state._tag === "Compound") {
236
+ const children = visit(state.states, path, insideParallel);
237
+ if (remaining > 0 && !insideParallel) {
238
+ const concrete = children.filter((child) => child._tag !== "History" && child._tag !== "Choice" && child._tag !== "Parallel");
239
+ if (concrete.some((child) => child.key === state.initial)) {
240
+ remaining -= 1;
241
+ const choice = Object.freeze({
242
+ _tag: "Choice",
243
+ key: `choice${remaining}`,
244
+ targets: Object.freeze(concrete.map((child) => `${path}.${child.key}`)),
245
+ selected: `${path}.${state.initial}`
246
+ });
247
+ return Object.freeze({
248
+ ...state,
249
+ initial: choice.key,
250
+ states: Object.freeze([...children, choice])
251
+ });
252
+ }
253
+ }
254
+ return Object.freeze({ ...state, states: children });
255
+ }
256
+ if (state._tag === "Parallel")
257
+ return Object.freeze({ ...state, states: visit(state.states, path, true) });
258
+ return state;
259
+ }));
260
+ return visit(roots, undefined, false);
261
+ };
262
+ const freezeModel = (roots, initial, events, transitions, historyScenarios = []) => Object.freeze({
263
+ roots,
264
+ initial,
265
+ events: Object.freeze(events.slice()),
266
+ transitions: Object.freeze(transitions.map((transition) => Object.freeze(transition))),
267
+ historyScenarios: Object.freeze(historyScenarios.slice())
268
+ });
269
+ const makeTransitionArbitrary = (roots, initial, events, maxTransitions, historyScenarios = []) => {
270
+ const states = flattenStates(roots);
271
+ const sourceOrder = new Map(states.map((state, index) => [state.path, index]));
272
+ const stateByPath = new Map(states.map((state) => [state.path, state]));
273
+ const initiallyActive = new Set(initialLeaves(stateByPath, initial).flatMap((leaf) => {
274
+ const parts = leaf.split(".");
275
+ return parts.map((_, index) => parts.slice(0, index + 1).join("."));
276
+ }));
277
+ const triggerOrder = (trigger) => trigger.type === "event"
278
+ ? events.indexOf(trigger.event)
279
+ : events.length + (trigger.type === "always" ? 0 : 1);
280
+ const orderTransitions = (transitions) => transitions.slice().sort((left, right) => sourceOrder.get(left.source) - sourceOrder.get(right.source) ||
281
+ triggerOrder(left.trigger) - triggerOrder(right.trigger));
282
+ const active = states.filter(({ node }) => node._tag !== "Final" && node._tag !== "History" && node._tag !== "Choice");
283
+ const mandatory = historyScenarios.flatMap((scenario) => [
284
+ {
285
+ source: scenario.mutation.source,
286
+ trigger: { type: "event", event: scenario.mutation.event },
287
+ target: scenario.mutation.target,
288
+ targetValue: scenario.mutation.value,
289
+ reenter: false
290
+ },
291
+ {
292
+ source: scenario.leave.source,
293
+ trigger: { type: "event", event: scenario.leave.event },
294
+ target: scenario.leave.target,
295
+ reenter: false
296
+ },
297
+ {
298
+ source: scenario.resume.source,
299
+ trigger: { type: "event", event: scenario.resume.event },
300
+ target: scenario.resume.target,
301
+ reenter: false
302
+ }
303
+ ]);
304
+ const mandatoryRegistrations = new Set(mandatory.map(({ source, trigger }) => `${source}\u0000${triggerKey(trigger)}`));
305
+ const reservedEvents = new Set(historyScenarios.flatMap(({ leave, mutation }) => [leave.event, mutation.event]));
306
+ const eventCandidates = active.flatMap(({ path: source, root }) => events.map((event) => ({
307
+ source,
308
+ trigger: { type: "event", event },
309
+ // `branch` addresses the source root while `full` replaces another root.
310
+ // Same-root targets may select any state, including a parallel state or
311
+ // a compound state whose initial descent enters a parallel state. The
312
+ // compiler expands those targets into every required orthogonal region.
313
+ targets: states.filter((target) => target.node._tag !== "History" && (target.root === root || target.parent === undefined))
314
+ .map(({ path }) => path)
315
+ }))).filter(({ source, trigger }) => !mandatoryRegistrations.has(`${source}\u0000${triggerKey(trigger)}`) &&
316
+ (trigger.type !== "event" || !reservedEvents.has(trigger.event)));
317
+ const exitsSourceTargets = (source) => states.filter((target, targetIndex) => {
318
+ if (targetIndex <= sourceOrder.get(source.path) || target.node._tag === "History" ||
319
+ target.node._tag === "Choice")
320
+ return false;
321
+ // A generated automatic transition must make its source inactive. This
322
+ // gives the independent oracle a structurally acyclic witness instead of
323
+ // relying on runtime iteration bounds. A later sibling under a compound
324
+ // parent exits the source branch; a later root replaces the whole root.
325
+ if (source.parent === undefined) {
326
+ return target.parent === undefined && target.root !== source.root;
327
+ }
328
+ return stateByPath.get(source.parent)?.node._tag === "Compound" && target.parent === source.parent;
329
+ }).map(({ path }) => path);
330
+ const automaticCandidates = historyScenarios.length === 0
331
+ ? [
332
+ ...active.flatMap((source) => source.node._tag !== "Atomic" ? [] : [{
333
+ source: source.path,
334
+ trigger: { type: "always" },
335
+ targets: exitsSourceTargets(source)
336
+ }]),
337
+ ...states.flatMap((source) => source.node._tag !== "Compound" && source.node._tag !== "Parallel" ? [] : [{
338
+ source: source.path,
339
+ trigger: { type: "done" },
340
+ targets: exitsSourceTargets(source)
341
+ }])
342
+ ].filter(({ targets }) => targets.length > 0)
343
+ : [];
344
+ const completesOnEntry = (path) => {
345
+ const state = stateByPath.get(path);
346
+ if (state.node._tag === "Final")
347
+ return true;
348
+ if (state.node._tag === "Compound") {
349
+ const node = state.node;
350
+ return node.states.some(({ key, _tag }) => key === node.initial && _tag === "Final");
351
+ }
352
+ if (state.node._tag === "Parallel") {
353
+ return state.node.states
354
+ .filter(({ _tag }) => _tag !== "History" && _tag !== "Choice")
355
+ .every((child) => completesOnEntry(`${path}.${child.key}`));
356
+ }
357
+ return false;
358
+ };
359
+ const materialize = (selected, allowTargetlessEvents = true) => {
360
+ if (selected.length === 0)
361
+ return FastCheck.constant([]);
362
+ const decisions = selected.map((candidate) => FastCheck.record({
363
+ // Targetless event transitions remain useful witnesses. Generated
364
+ // automatic transitions always exit their source so stabilization is
365
+ // acyclic by construction; targetless automatic semantics are covered
366
+ // by focused examples rather than mixed into the finite-model oracle.
367
+ targetIndex: FastCheck.integer({
368
+ min: candidate.trigger.type === "event" && allowTargetlessEvents ? 0 : 1,
369
+ max: candidate.targets.length
370
+ }),
371
+ targetValueOffset: FastCheck.integer({ min: 0, max: 2 }),
372
+ reenter: FastCheck.boolean()
373
+ }));
374
+ return FastCheck.tuple(...decisions).map((values) => selected.map((candidate, index) => {
375
+ const decision = values[index];
376
+ const target = decision.targetIndex === 0 ? undefined : candidate.targets[decision.targetIndex - 1];
377
+ const targetState = target === undefined ? undefined : states.find(({ path }) => path === target);
378
+ const targetValue = targetState?.node._tag === "Atomic" &&
379
+ decision.targetValueOffset !== 0
380
+ ? targetState.node.value + decision.targetValueOffset
381
+ : undefined;
382
+ const targetFields = target === undefined
383
+ ? {}
384
+ : { target, ...(targetValue === undefined ? {} : { targetValue }) };
385
+ const trigger = candidate.trigger;
386
+ return trigger.type === "event"
387
+ ? { source: candidate.source, trigger, reenter: decision.reenter, ...targetFields }
388
+ : { source: candidate.source, trigger, ...targetFields };
389
+ }));
390
+ };
391
+ const optionalBudget = maxTransitions - mandatory.length;
392
+ const general = FastCheck.subarray([...eventCandidates, ...automaticCandidates], {
393
+ minLength: 0,
394
+ maxLength: Math.min(optionalBudget, eventCandidates.length + automaticCandidates.length)
395
+ }).chain((selected) => materialize(selected).map((transitions) => orderTransitions([...mandatory, ...transitions])));
396
+ if (mandatory.length > 0 || optionalBudget < 2 || automaticCandidates.length === 0)
397
+ return general;
398
+ // Bias one branch toward a reachable public-event -> automatic-transition
399
+ // chain. The automatic edge itself still moves forward in document order,
400
+ // so combining the two trigger kinds cannot introduce an automatic cycle.
401
+ const chainCandidates = eventCandidates.flatMap((eventCandidate) => {
402
+ const source = stateByPath.get(eventCandidate.source);
403
+ if (source.node._tag !== "Atomic" || !initiallyActive.has(source.path))
404
+ return [];
405
+ return automaticCandidates.flatMap((automaticCandidate) => {
406
+ if (automaticCandidate.source === eventCandidate.source ||
407
+ !eventCandidate.targets.includes(automaticCandidate.source) ||
408
+ (automaticCandidate.trigger.type === "done" && !completesOnEntry(automaticCandidate.source)))
409
+ return [];
410
+ return [{
411
+ event: { ...eventCandidate, targets: [automaticCandidate.source] },
412
+ automatic: automaticCandidate
413
+ }];
414
+ });
415
+ });
416
+ if (chainCandidates.length === 0)
417
+ return general;
418
+ const chain = FastCheck.constantFrom(...chainCandidates).chain(({ automatic, event }) => materialize([event, automatic], false).map((transitions) => orderTransitions(transitions)));
419
+ return FastCheck.oneof(general, chain);
420
+ };
421
+ /**
422
+ * Generates bounded, structurally valid hierarchical statechart
423
+ * models.
424
+ *
425
+ * Generation is composed from shrinkable topology, initial-state, event, and
426
+ * transition decisions. Paths and transition candidates are rebuilt after a
427
+ * topology shrink, so a shrink can never retain a dangling source or target.
428
+ * Generated automatic transitions are acyclic by construction: `always`
429
+ * transitions leave atomic sources and completion transitions leave compound
430
+ * or parallel sources. Separate focused witnesses cover cyclic stabilization.
431
+ *
432
+ * @category constructors
433
+ * @since 0.4.0
434
+ */
435
+ export const finiteModels = (options = {}) => {
436
+ const limits = resolveOptions(options);
437
+ const rawRoots = FastCheck.array(rawStateArbitrary(1, limits), {
438
+ minLength: 1,
439
+ maxLength: limits.maxRoots
440
+ });
441
+ const arbitrary = rawRoots.chain((raw) => {
442
+ const activeRoots = normalizeStates(raw);
443
+ return FastCheck.tuple(FastCheck.integer({ min: 0, max: activeRoots.length - 1 }), FastCheck.integer({ min: 1, max: limits.maxEvents }), FastCheck.array(FastCheck.constantFrom("shallow", "deep"), {
444
+ minLength: 0,
445
+ maxLength: limits.maxHistoryStates
446
+ })).chain(([initialIndex, eventCount, historyDecisions]) => {
447
+ const events = Array.from({ length: eventCount }, (_, index) => `Event${index}`);
448
+ const initial = activeRoots[initialIndex].key;
449
+ const generated = addGeneratedHistory(activeRoots, initial, limits.maxHistoryStates, limits.maxTransitions, events, historyDecisions);
450
+ const roots = generated.scenarios.length === 0
451
+ ? addGeneratedChoices(generated.roots, limits.maxChoiceStates)
452
+ : generated.roots;
453
+ return makeTransitionArbitrary(roots, initial, events, limits.maxTransitions, generated.scenarios).map((transitions) => freezeModel(roots, initial, events, transitions, generated.scenarios));
454
+ });
455
+ });
456
+ return {
457
+ arbitrary,
458
+ diagnostics: {
459
+ limits,
460
+ guarantees: {
461
+ compoundOnly: false,
462
+ parallelStates: true,
463
+ historyStates: true,
464
+ historyLeaveResumeSequences: true,
465
+ historyValueScenarios: true,
466
+ choiceStates: true,
467
+ choiceInitialWitnesses: true,
468
+ structurallyValid: true,
469
+ shrinkPreservesValidity: true,
470
+ eventlessTransitions: true,
471
+ acyclicAutomaticTransitions: true
472
+ }
473
+ }
474
+ };
475
+ };
476
+ const isValidKey = (key) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(key);
477
+ const validateModel = (model) => {
478
+ if (!Array.isArray(model.roots) || model.roots.length < 1 || model.roots.length > 3) {
479
+ throw new Error("MachineTest.compileModel expected between one and three root states");
480
+ }
481
+ const visit = (states, parent) => {
482
+ if (states.length === 0) {
483
+ throw new Error(`MachineTest.compileModel expected ${parent ?? "the model"} to contain a state`);
484
+ }
485
+ const siblingKeys = new Set();
486
+ for (const node of states) {
487
+ if (!isValidKey(node.key)) {
488
+ throw new Error(`MachineTest.compileModel received invalid state key "${node.key}"`);
489
+ }
490
+ if (siblingKeys.has(node.key)) {
491
+ throw new Error(`MachineTest.compileModel received duplicate state key "${node.key}"`);
492
+ }
493
+ siblingKeys.add(node.key);
494
+ const path = parent === undefined ? node.key : `${parent}.${node.key}`;
495
+ if (node._tag !== "History" && node._tag !== "Choice" && !Number.isSafeInteger(node.value)) {
496
+ throw new Error(`MachineTest.compileModel expected state "${path}" value to be a safe integer`);
497
+ }
498
+ if (node._tag === "History") {
499
+ if (parent === undefined) {
500
+ throw new Error(`MachineTest.compileModel received root history state "${path}"`);
501
+ }
502
+ if (node.history !== "shallow" && node.history !== "deep") {
503
+ throw new Error(`MachineTest.compileModel received invalid history mode at "${path}"`);
504
+ }
505
+ continue;
506
+ }
507
+ if (node._tag === "Choice") {
508
+ if (parent === undefined || node.targets.length === 0 || !node.targets.includes(node.selected)) {
509
+ throw new Error(`MachineTest.compileModel received invalid choice state "${path}"`);
510
+ }
511
+ continue;
512
+ }
513
+ if (node._tag === "Compound") {
514
+ if (!node.states.some((child) => child.key === node.initial && child._tag !== "History")) {
515
+ throw new Error(`MachineTest.compileModel received unknown initial child "${node.initial}" for "${path}"`);
516
+ }
517
+ visit(node.states, path);
518
+ }
519
+ else if (node._tag === "Parallel") {
520
+ const regions = node.states.filter((child) => child._tag !== "History" && child._tag !== "Choice");
521
+ if (regions.length < 2 || regions.length > 3) {
522
+ throw new Error(`MachineTest.compileModel expected parallel state "${path}" to contain two or three regions`);
523
+ }
524
+ if (typeof node.output !== "string") {
525
+ throw new Error(`MachineTest.compileModel expected parallel state "${path}" output to be a string`);
526
+ }
527
+ visit(node.states, path);
528
+ }
529
+ else if (node._tag === "Final" && typeof node.output !== "string") {
530
+ throw new Error(`MachineTest.compileModel expected final state "${path}" output to be a string`);
531
+ }
532
+ else if (node._tag !== "Atomic" && node._tag !== "Final") {
533
+ throw new Error(`MachineTest.compileModel received unsupported state type at "${path}"`);
534
+ }
535
+ }
536
+ };
537
+ visit(model.roots, undefined);
538
+ if (!model.roots.some((root) => root.key === model.initial)) {
539
+ throw new Error(`MachineTest.compileModel received unknown initial root "${model.initial}"`);
540
+ }
541
+ const events = new Set();
542
+ for (const event of model.events) {
543
+ if (typeof event !== "string" || event.length === 0 || events.has(event)) {
544
+ throw new Error(`MachineTest.compileModel received invalid or duplicate event tag "${String(event)}"`);
545
+ }
546
+ events.add(event);
547
+ }
548
+ if (events.size === 0) {
549
+ throw new Error("MachineTest.compileModel expected at least one event tag");
550
+ }
551
+ const flattened = flattenStates(model.roots);
552
+ const byPath = new Map(flattened.map((state) => [state.path, state]));
553
+ for (const state of flattened) {
554
+ if (state.node._tag !== "Choice")
555
+ continue;
556
+ const owner = state.parent === undefined ? undefined : byPath.get(state.parent);
557
+ if (owner?.node._tag !== "Compound") {
558
+ throw new Error(`MachineTest.compileModel expected choice state "${state.path}" to belong to a compound state`);
559
+ }
560
+ for (const targetPath of state.node.targets) {
561
+ const target = byPath.get(targetPath);
562
+ if (target === undefined || target.node._tag === "History" || target.node._tag === "Choice" ||
563
+ target.parent !== owner.path) {
564
+ throw new Error(`MachineTest.compileModel received invalid choice target "${targetPath}"`);
565
+ }
566
+ }
567
+ }
568
+ for (const state of flattened) {
569
+ if (state.node._tag !== "History")
570
+ continue;
571
+ const owner = state.parent === undefined ? undefined : byPath.get(state.parent);
572
+ const fallback = byPath.get(state.node.fallback);
573
+ if (owner === undefined ||
574
+ (owner.node._tag !== "Compound" && owner.node._tag !== "Parallel") ||
575
+ fallback === undefined ||
576
+ fallback.node._tag === "History" ||
577
+ fallback.path === owner.path ||
578
+ !isPathInSubtree(fallback.path, owner.path)) {
579
+ throw new Error(`MachineTest.compileModel expected history state "${state.path}" fallback to be a concrete descendant of "${state.parent}"`);
580
+ }
581
+ }
582
+ const registrations = new Set();
583
+ const transitionsByRegistration = new Map();
584
+ for (const transition of model.transitions) {
585
+ const source = byPath.get(transition.source);
586
+ if (source === undefined || source.node._tag === "Final" || source.node._tag === "History" ||
587
+ source.node._tag === "Choice") {
588
+ throw new Error(`MachineTest.compileModel received invalid transition source "${transition.source}"`);
589
+ }
590
+ if (transition.trigger.type === "event" && !events.has(transition.trigger.event)) {
591
+ throw new Error(`MachineTest.compileModel received unknown transition event "${transition.trigger.event}"`);
592
+ }
593
+ if (transition.trigger.type === "done" && source.node._tag !== "Compound" && source.node._tag !== "Parallel") {
594
+ throw new Error(`MachineTest.compileModel received completion transition for non-composite "${transition.source}"`);
595
+ }
596
+ if (transition.trigger.type !== "event" && "reenter" in transition) {
597
+ throw new Error(`MachineTest.compileModel received event-only reenter option from "${transition.source}"`);
598
+ }
599
+ const registration = `${transition.source}\u0000${triggerKey(transition.trigger)}`;
600
+ if (registrations.has(registration)) {
601
+ throw new Error(`MachineTest.compileModel received duplicate transition for "${transition.source}" on ` +
602
+ `"${triggerKey(transition.trigger)}"`);
603
+ }
604
+ registrations.add(registration);
605
+ transitionsByRegistration.set(registration, transition);
606
+ if (transition.target !== undefined) {
607
+ const target = byPath.get(transition.target);
608
+ if (target === undefined) {
609
+ throw new Error(`MachineTest.compileModel received unknown transition target "${transition.target}"`);
610
+ }
611
+ if (target.node._tag !== "History" && target.root !== source.root && target.parent !== undefined) {
612
+ throw new Error(`MachineTest.compileModel expected cross-root target "${transition.target}" to select its root "${target.root}"`);
613
+ }
614
+ }
615
+ if (transition.targetValue !== undefined &&
616
+ (!Number.isSafeInteger(transition.targetValue) || transition.target === undefined ||
617
+ byPath.get(transition.target)?.node._tag === "History" ||
618
+ byPath.get(transition.target)?.node._tag === "Choice")) {
619
+ throw new Error(`MachineTest.compileModel received invalid target value for transition from "${transition.source}"`);
620
+ }
621
+ }
622
+ const initiallyActiveLeaves = initialLeaves(byPath, model.initial);
623
+ const initiallyActive = new Set(initiallyActiveLeaves.flatMap((leaf) => {
624
+ const parts = leaf.split(".");
625
+ return parts.map((_, index) => parts.slice(0, index + 1).join("."));
626
+ }));
627
+ const scenarioHistories = new Set();
628
+ const scenarioEvents = new Set();
629
+ const scenarioRegistrations = new Set();
630
+ for (const scenario of model.historyScenarios ?? []) {
631
+ const history = byPath.get(scenario.history);
632
+ const mutationState = byPath.get(scenario.mutation.source);
633
+ if (history?.node._tag !== "History" || history.parent !== scenario.owner ||
634
+ history.node.history !== scenario.historyType) {
635
+ throw new Error(`MachineTest.compileModel received invalid history scenario for "${scenario.history}"`);
636
+ }
637
+ if (scenarioHistories.has(scenario.history)) {
638
+ throw new Error(`MachineTest.compileModel received duplicate history scenario for "${scenario.history}"`);
639
+ }
640
+ scenarioHistories.add(scenario.history);
641
+ const owner = byPath.get(scenario.owner);
642
+ const ownerNode = owner.node;
643
+ let expectedMutationSource;
644
+ if (scenario.historyType === "deep") {
645
+ expectedMutationSource = initialLeaves(byPath, owner.path)[0];
646
+ }
647
+ else if (ownerNode._tag === "Compound") {
648
+ if (ownerNode.states.some((child) => child.key === ownerNode.initial && child._tag === "Atomic")) {
649
+ expectedMutationSource = `${owner.path}.${ownerNode.initial}`;
650
+ }
651
+ }
652
+ else if (ownerNode._tag === "Parallel") {
653
+ const directAtomic = ownerNode.states.find((child) => child._tag === "Atomic");
654
+ if (directAtomic !== undefined)
655
+ expectedMutationSource = `${owner.path}.${directAtomic.key}`;
656
+ }
657
+ if (mutationState?.node._tag !== "Atomic" || scenario.mutation.target !== scenario.mutation.source ||
658
+ scenario.mutation.value === mutationState.node.value || !initiallyActive.has(mutationState.path) ||
659
+ scenario.mutation.source !== expectedMutationSource) {
660
+ throw new Error(`MachineTest.compileModel received invalid history mutation for "${scenario.history}"`);
661
+ }
662
+ if (!Array.isArray(scenario.events) || scenario.events.length !== 3 ||
663
+ scenario.resume.target !== scenario.history || scenario.leave.event !== scenario.resume.event ||
664
+ scenario.events[0] !== scenario.mutation.event || scenario.events[1] !== scenario.leave.event ||
665
+ scenario.events[2] !== scenario.resume.event) {
666
+ throw new Error(`MachineTest.compileModel received inconsistent history events for "${scenario.history}"`);
667
+ }
668
+ const expectedOutside = model.roots
669
+ .filter((root) => root.key !== model.initial)
670
+ .find((root) => initialLeaves(byPath, root.key).length > 0);
671
+ if (scenario.leave.source !== initialLeaves(byPath, owner.path)[0] ||
672
+ scenario.leave.target !== expectedOutside?.key ||
673
+ scenario.resume.source !==
674
+ (expectedOutside === undefined ? undefined : initialLeaves(byPath, expectedOutside.key)[0])) {
675
+ throw new Error(`MachineTest.compileModel received invalid history transfer for "${scenario.history}"`);
676
+ }
677
+ const expectedTransitions = [
678
+ [scenario.mutation, scenario.mutation.value],
679
+ [scenario.leave],
680
+ [scenario.resume]
681
+ ];
682
+ if (scenarioEvents.has(scenario.mutation.event) || scenarioEvents.has(scenario.leave.event)) {
683
+ throw new Error(`MachineTest.compileModel received duplicate history scenario event for "${scenario.history}"`);
684
+ }
685
+ scenarioEvents.add(scenario.mutation.event);
686
+ scenarioEvents.add(scenario.leave.event);
687
+ for (const [expected, targetValue] of expectedTransitions) {
688
+ const registration = `${expected.source}\u0000${triggerKey({ type: "event", event: expected.event })}`;
689
+ if (scenarioRegistrations.has(registration)) {
690
+ throw new Error(`MachineTest.compileModel received duplicate history witness for "${scenario.history}"`);
691
+ }
692
+ scenarioRegistrations.add(registration);
693
+ const transition = transitionsByRegistration.get(registration);
694
+ if (transition?.trigger.type !== "event" || transition.trigger.event !== expected.event ||
695
+ transition.target !== expected.target || !("reenter" in transition) || transition.reenter ||
696
+ transition.targetValue !== targetValue) {
697
+ throw new Error(`MachineTest.compileModel could not replay history scenario for "${scenario.history}"`);
698
+ }
699
+ }
700
+ }
701
+ return flattened;
702
+ };
703
+ const stateTag = (path) => `State_${path.replaceAll(".", "_")}`;
704
+ const stateValue = (state, value) => {
705
+ if (state.node._tag === "History" || state.node._tag === "Choice") {
706
+ throw new Error(`MachineTest.compileModel cannot construct a value for pseudo-state "${state.path}"`);
707
+ }
708
+ return { _tag: stateTag(state.path), value: value ?? state.node.value };
709
+ };
710
+ const runtimeTargetPath = (byPath, sourcePath, requestedPath) => {
711
+ const source = byPath.get(sourcePath);
712
+ const requested = byPath.get(requestedPath);
713
+ if (requested.node._tag === "History")
714
+ return requested.parent;
715
+ if (requested.node._tag === "Choice")
716
+ return runtimeTargetPath(byPath, sourcePath, requested.node.selected);
717
+ if (source.root !== requested.root)
718
+ return requested.path;
719
+ const initial = (path) => {
720
+ const current = byPath.get(path);
721
+ if (current.node._tag === "Parallel") {
722
+ if (sourcePath !== current.path && !sourcePath.startsWith(`${current.path}.`)) {
723
+ return current.path;
724
+ }
725
+ const child = sourcePath === current.path
726
+ ? current.node.states[0].key
727
+ : sourcePath.slice(current.path.length + 1).split(".")[0];
728
+ return initial(`${current.path}.${child}`);
729
+ }
730
+ return current.node._tag === "Compound" ? initial(`${current.path}.${current.node.initial}`) : current.path;
731
+ };
732
+ const inspect = (path) => {
733
+ const current = byPath.get(path);
734
+ if (current.node._tag === "Parallel" && sourcePath !== current.path && !sourcePath.startsWith(`${current.path}.`)) {
735
+ return current.path;
736
+ }
737
+ if (current.path === requestedPath)
738
+ return initial(current.path);
739
+ const next = requestedPath.slice(current.path.length + 1).split(".")[0];
740
+ return inspect(`${current.path}.${next}`);
741
+ };
742
+ return inspect(source.root);
743
+ };
744
+ const makeStateTree = (states, parent) => {
745
+ const tree = Object.create(null);
746
+ for (const node of states) {
747
+ const path = parent === undefined ? node.key : `${parent}.${node.key}`;
748
+ if (node._tag === "History") {
749
+ tree[node.key] = {
750
+ type: "history",
751
+ ...(node.history === "deep" ? { history: "deep" } : {})
752
+ };
753
+ continue;
754
+ }
755
+ if (node._tag === "Choice") {
756
+ tree[node.key] = { type: "choice" };
757
+ continue;
758
+ }
759
+ const schema = Schema.TaggedStruct(stateTag(path), { value: Schema.Number });
760
+ if (node._tag === "Atomic") {
761
+ tree[node.key] = schema;
762
+ }
763
+ else if (node._tag === "Final") {
764
+ tree[node.key] = { schema, type: "final", output: Schema.Literal(node.output) };
765
+ }
766
+ else if (node._tag === "Compound") {
767
+ tree[node.key] = {
768
+ schema,
769
+ initial: node.initial,
770
+ states: makeStateTree(node.states, path)
771
+ };
772
+ }
773
+ else {
774
+ tree[node.key] = {
775
+ schema,
776
+ type: "parallel",
777
+ output: Schema.Literal(node.output),
778
+ states: makeStateTree(node.states, path)
779
+ };
780
+ }
781
+ }
782
+ return tree;
783
+ };
784
+ const selectSnapshot = (builder, path, byPath, requestedParts, index, sourcePath, requestedValue) => {
785
+ const state = byPath.get(path);
786
+ if (state.node._tag === "History") {
787
+ throw new Error(`MachineTest.compileModel cannot construct active history state "${path}"`);
788
+ }
789
+ if (state.node._tag === "Choice") {
790
+ return builder[state.node.key]();
791
+ }
792
+ const method = builder[state.node.key];
793
+ const value = stateValue(state, path === requestedParts?.join(".") ? requestedValue : undefined);
794
+ if (state.node._tag === "Atomic" || state.node._tag === "Final")
795
+ return method(value);
796
+ const requestedChild = requestedParts?.[index + 1];
797
+ if (state.node._tag === "Parallel") {
798
+ const parallel = state.node;
799
+ const sourceInside = sourcePath === path || sourcePath?.startsWith(`${path}.`) === true;
800
+ if (sourceInside) {
801
+ const childKey = requestedChild ?? (sourcePath === path
802
+ ? parallel.states.find((child) => child._tag !== "History" && child._tag !== "Choice").key
803
+ : sourcePath.slice(path.length + 1).split(".")[0]);
804
+ return method(value, (children) => selectSnapshot(children, `${path}.${childKey}`, byPath, requestedChild === undefined ? undefined : requestedParts, index + 1, sourcePath, requestedValue));
805
+ }
806
+ return method(value, (children) => {
807
+ let selected = children;
808
+ for (const child of parallel.states.filter((child) => child._tag !== "History" && child._tag !== "Choice")) {
809
+ const isRequestedRegion = requestedChild === child.key;
810
+ selected = selectSnapshot(selected, `${path}.${child.key}`, byPath, isRequestedRegion ? requestedParts : undefined, index + 1, sourcePath, requestedValue);
811
+ }
812
+ return selected;
813
+ });
814
+ }
815
+ const childKey = requestedChild ?? state.node.initial;
816
+ const childPath = `${path}.${childKey}`;
817
+ return method(value, (children) => selectSnapshot(children, childPath, byPath, requestedChild === undefined ? undefined : requestedParts, index + 1, sourcePath, requestedValue));
818
+ };
819
+ const findSnapshot = (snapshot, path) => {
820
+ if (typeof snapshot !== "object" || snapshot === null)
821
+ return undefined;
822
+ const current = snapshot;
823
+ if (current.path === path)
824
+ return snapshot;
825
+ if (current.state !== undefined) {
826
+ const found = findSnapshot(current.state, path);
827
+ if (found !== undefined)
828
+ return found;
829
+ }
830
+ if (typeof current.states === "object" && current.states !== null) {
831
+ for (const child of Object.values(current.states)) {
832
+ const found = findSnapshot(child, path);
833
+ if (found !== undefined)
834
+ return found;
835
+ }
836
+ }
837
+ return undefined;
838
+ };
839
+ const selectHistoryTarget = (builder, path) => {
840
+ const parts = path.split(".");
841
+ let current = builder;
842
+ for (let index = 0; index < parts.length - 1; index++)
843
+ current = current[parts[index]];
844
+ return current[parts[parts.length - 1]]();
845
+ };
846
+ const makeHandlers = (states, parent, byPath, transitions) => {
847
+ const handlers = Object.create(null);
848
+ for (const node of states) {
849
+ const path = parent === undefined ? node.key : `${parent}.${node.key}`;
850
+ if (node._tag === "History")
851
+ continue;
852
+ if (node._tag === "Choice") {
853
+ handlers[node.key] = {
854
+ choice: {
855
+ targets: node.targets,
856
+ transition: ({ target }) => {
857
+ const selected = byPath.get(node.selected);
858
+ const parts = selected.path.split(".");
859
+ const builder = selected.root === byPath.get(path).root ? target.branch : target.full;
860
+ return selectSnapshot(builder, parts[0], byPath, parts, 0, path);
861
+ }
862
+ }
863
+ };
864
+ continue;
865
+ }
866
+ if (node._tag === "Final") {
867
+ handlers[node.key] = { output: () => node.output };
868
+ continue;
869
+ }
870
+ const on = Object.create(null);
871
+ let always;
872
+ let onDone;
873
+ for (const transition of transitions.values()) {
874
+ if (transition.source !== path)
875
+ continue;
876
+ const config = {
877
+ ...("reenter" in transition ? { reenter: transition.reenter } : {}),
878
+ ...(transition.target === undefined
879
+ ? {}
880
+ : {
881
+ targets: [
882
+ byPath.get(transition.target).node._tag === "History" ||
883
+ byPath.get(transition.target).node._tag === "Choice"
884
+ ? transition.target
885
+ : runtimeTargetPath(byPath, path, transition.target)
886
+ ]
887
+ }),
888
+ transition: ({ target }) => {
889
+ if (transition.target === undefined)
890
+ return undefined;
891
+ const targetState = byPath.get(transition.target);
892
+ if (targetState.node._tag === "History")
893
+ return selectHistoryTarget(target.history, targetState.path);
894
+ const parts = transition.target.split(".");
895
+ const builder = targetState.root === byPath.get(path).root ? target.branch : target.full;
896
+ return selectSnapshot(builder, parts[0], byPath, parts, 0, path, transition.targetValue);
897
+ }
898
+ };
899
+ if (transition.trigger.type === "event")
900
+ on[transition.trigger.event] = config;
901
+ else if (transition.trigger.type === "always")
902
+ always = config;
903
+ else
904
+ onDone = config;
905
+ }
906
+ const history = node._tag === "Compound" || node._tag === "Parallel"
907
+ ? Object.fromEntries(node.states.flatMap((child) => {
908
+ if (child._tag !== "History")
909
+ return [];
910
+ return [[child.key, {
911
+ default: ({ target }) => {
912
+ const fallback = byPath.get(child.fallback);
913
+ const parts = child.fallback.split(".");
914
+ const completeRoot = selectSnapshot(target, fallback.root, byPath, parts, 0);
915
+ if (findSnapshot(completeRoot, path) === undefined) {
916
+ throw new Error(`MachineTest.compileModel could not construct history fallback for "${path}.${child.key}"`);
917
+ }
918
+ return completeRoot;
919
+ }
920
+ }]];
921
+ }))
922
+ : {};
923
+ handlers[node.key] = {
924
+ ...(Object.keys(on).length === 0 ? {} : { on }),
925
+ ...(always === undefined ? {} : { always }),
926
+ ...(onDone === undefined ? {} : { onDone }),
927
+ ...(node._tag === "Compound" || node._tag === "Parallel"
928
+ ? {
929
+ ...(node._tag === "Parallel" ? { output: () => node.output } : {}),
930
+ ...(Object.keys(history).length === 0 ? {} : { history }),
931
+ ...(node._tag === "Compound"
932
+ ? {
933
+ initial: () => stateValue(byPath.get(`${path}.${node.initial}`))
934
+ }
935
+ : {
936
+ initial: () => Object.fromEntries(node.states
937
+ .filter((child) => child._tag !== "History" && child._tag !== "Choice")
938
+ .map((child) => [child.key, stateValue(byPath.get(`${path}.${child.key}`))]))
939
+ }),
940
+ states: makeHandlers(node.states, path, byPath, transitions)
941
+ }
942
+ : {})
943
+ };
944
+ }
945
+ return handlers;
946
+ };
947
+ /**
948
+ * Compiles a finite model into a real machine using only public definition,
949
+ * construction, and handler APIs.
950
+ *
951
+ * Hand-authored models are validated before compilation. This compiler is a
952
+ * testing adapter, not the independent reference interpreter used by later
953
+ * conformance stages.
954
+ *
955
+ * @category constructors
956
+ * @since 0.4.0
957
+ */
958
+ export const compileModel = (model) => {
959
+ const flattened = validateModel(model);
960
+ const byPath = new Map(flattened.map((state) => [state.path, state]));
961
+ const stateTree = makeStateTree(model.roots, undefined);
962
+ const defined = Machine.defineStates(stateTree);
963
+ const eventSchemas = model.events.map((event) => Schema.TaggedStruct(event, {}));
964
+ const initial = byPath.get(model.initial);
965
+ const machine = Machine.make({
966
+ states: defined.states,
967
+ events: eventSchemas,
968
+ initial: () => selectSnapshot(defined.initial, initial.path, byPath, [initial.path], 0)
969
+ });
970
+ const transitions = new Map(model.transitions.map((transition) => [
971
+ `${transition.source}\u0000${triggerKey(transition.trigger)}`,
972
+ transition
973
+ ]));
974
+ return machine.handle(makeHandlers(model.roots, undefined, byPath, transitions));
975
+ };
976
+ //# sourceMappingURL=finiteModel.js.map