@typeonce/effect-machine 0.13.0 → 0.14.1

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 (59) hide show
  1. package/README.md +74 -26
  2. package/dist/Machine.d.ts +379 -188
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +119 -53
  5. package/dist/Machine.js.map +1 -1
  6. package/dist/internal/machine/executionPlan.d.ts.map +1 -1
  7. package/dist/internal/machine/executionPlan.js +14 -6
  8. package/dist/internal/machine/executionPlan.js.map +1 -1
  9. package/dist/internal/machine/machine.d.ts +3 -1
  10. package/dist/internal/machine/machine.d.ts.map +1 -1
  11. package/dist/internal/machine/machine.js +318 -26
  12. package/dist/internal/machine/machine.js.map +1 -1
  13. package/dist/internal/machine/planner.d.ts +13 -0
  14. package/dist/internal/machine/planner.d.ts.map +1 -1
  15. package/dist/internal/machine/planner.js +15 -7
  16. package/dist/internal/machine/planner.js.map +1 -1
  17. package/dist/internal/machine/topology.d.ts +12 -0
  18. package/dist/internal/machine/topology.d.ts.map +1 -1
  19. package/dist/internal/machine/topology.js +17 -10
  20. package/dist/internal/machine/topology.js.map +1 -1
  21. package/dist/internal/testing/machine/exploration.d.ts.map +1 -1
  22. package/dist/internal/testing/machine/exploration.js +11 -2
  23. package/dist/internal/testing/machine/exploration.js.map +1 -1
  24. package/dist/internal/testing/machine/finiteModel.d.ts.map +1 -1
  25. package/dist/internal/testing/machine/finiteModel.js +72 -66
  26. package/dist/internal/testing/machine/finiteModel.js.map +1 -1
  27. package/dist/internal/testing/machine/transitionCoverage.d.ts +20 -0
  28. package/dist/internal/testing/machine/transitionCoverage.d.ts.map +1 -0
  29. package/dist/internal/testing/machine/transitionCoverage.js +80 -0
  30. package/dist/internal/testing/machine/transitionCoverage.js.map +1 -0
  31. package/dist/internal/testing/machine/verification.d.ts.map +1 -1
  32. package/dist/internal/testing/machine/verification.js +171 -33
  33. package/dist/internal/testing/machine/verification.js.map +1 -1
  34. package/dist/testing/MachineTest.d.ts +97 -22
  35. package/dist/testing/MachineTest.d.ts.map +1 -1
  36. package/dist/testing/MachineTest.js +58 -16
  37. package/dist/testing/MachineTest.js.map +1 -1
  38. package/dist/unstable/cluster/ClusterMachine.d.ts +4 -1
  39. package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -1
  40. package/dist/unstable/cluster/ClusterMachine.js +4 -1
  41. package/dist/unstable/cluster/ClusterMachine.js.map +1 -1
  42. package/dist/unstable/reactivity/AtomMachine.d.ts +12 -3
  43. package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
  44. package/dist/unstable/reactivity/AtomMachine.js +12 -3
  45. package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
  46. package/docs/agent-guide.md +139 -57
  47. package/package.json +1 -1
  48. package/src/Machine.ts +750 -336
  49. package/src/internal/machine/executionPlan.ts +22 -7
  50. package/src/internal/machine/machine.ts +420 -33
  51. package/src/internal/machine/planner.ts +44 -13
  52. package/src/internal/machine/topology.ts +38 -12
  53. package/src/internal/testing/machine/exploration.ts +10 -2
  54. package/src/internal/testing/machine/finiteModel.ts +106 -73
  55. package/src/internal/testing/machine/transitionCoverage.ts +116 -0
  56. package/src/internal/testing/machine/verification.ts +216 -58
  57. package/src/testing/MachineTest.ts +118 -28
  58. package/src/unstable/cluster/ClusterMachine.ts +4 -1
  59. package/src/unstable/reactivity/AtomMachine.ts +12 -3
@@ -70,6 +70,7 @@ export type MicrostepPlan<State, Event, E, R> = {
70
70
  readonly source: string
71
71
  readonly trigger: Machine.TransitionTrigger
72
72
  readonly reenter: boolean
73
+ readonly branchIndex: number
73
74
  readonly target: string | undefined
74
75
  readonly resolvedTarget: string | undefined
75
76
  }>
@@ -104,6 +105,16 @@ export type TransitionHandler<States extends Machine.StateSchemas, E, R, Context
104
105
  enqueue: Enqueue<any, any>
105
106
  ) => Machine.HandlerResult<States, E, R>
106
107
 
108
+ type TransitionEvaluation<States extends Machine.StateSchemas, E, R> = {
109
+ readonly result: Machine.HandlerResult<States, E, R>
110
+ readonly branchIndex: number
111
+ }
112
+
113
+ type TransitionEvaluator<States extends Machine.StateSchemas, E, R, Context> = (
114
+ context: Context,
115
+ enqueue: Enqueue<any, any>
116
+ ) => TransitionEvaluation<States, E, R>
117
+
107
118
  const rootMachineReferences = new WeakMap<Machine.Any, MachineReferences<any, any>>()
108
119
  const scopedMachineReferences = new WeakMap<Machine.Any, MachineReferences<any, any>>()
109
120
 
@@ -142,12 +153,14 @@ type EventTransition<States extends Machine.StateSchemas, E, R, Context> =
142
153
  readonly reenter?: boolean
143
154
  readonly targets?: ReadonlyArray<string>
144
155
  readonly transition: TransitionHandler<States, E, R, Context>
156
+ readonly evaluate?: TransitionEvaluator<States, E, R, Context>
145
157
  }
146
158
 
147
159
  export type MicrostepTransition<States extends Machine.StateSchemas, E, R, Context> = {
148
160
  readonly reenter: boolean
149
161
  readonly targets: ReadonlyArray<string> | undefined
150
162
  readonly transition: TransitionHandler<States, E, R, Context>
163
+ readonly evaluate: TransitionEvaluator<States, E, R, Context> | undefined
151
164
  }
152
165
 
153
166
  export const normalizeTransition = <States extends Machine.StateSchemas, E, R, Context>(
@@ -157,11 +170,12 @@ export const normalizeTransition = <States extends Machine.StateSchemas, E, R, C
157
170
  return undefined
158
171
  }
159
172
  return typeof transition === "function"
160
- ? { reenter: false, targets: undefined, transition }
173
+ ? { reenter: false, targets: undefined, transition, evaluate: undefined }
161
174
  : {
162
175
  reenter: transition.reenter === true,
163
176
  targets: transition.targets,
164
- transition: transition.transition
177
+ transition: transition.transition,
178
+ evaluate: transition.evaluate
165
179
  }
166
180
  }
167
181
 
@@ -195,12 +209,16 @@ const collectTransition = <
195
209
  >(
196
210
  machine: Machine.Any,
197
211
  transition: TransitionHandler<States, E, R, Context>,
198
- context: Context
212
+ context: Context,
213
+ evaluate?: TransitionEvaluator<States, E, R, Context>
199
214
  ) => {
200
215
  const collected = makeCollector<Event>(machine)
201
- const result = transition(context, collected.enqueue)
216
+ const evaluated = evaluate === undefined
217
+ ? { result: transition(context, collected.enqueue), branchIndex: 0 }
218
+ : evaluate(context, collected.enqueue)
202
219
  return {
203
- state: isNoTarget(result) ? undefined : result,
220
+ state: isNoTarget(evaluated.result) ? undefined : evaluated.result,
221
+ branchIndex: evaluated.branchIndex,
204
222
  commands: collected.commands,
205
223
  raisedEvents: collected.raisedEvents,
206
224
  emittedEvents: collected.emittedEvents
@@ -504,6 +522,7 @@ export type SelectedTransition<States extends Machine.StateSchemas, E, R, Contex
504
522
 
505
523
  export type EvaluatedTransition<States extends Machine.StateSchemas, Event, E, R, Context> = {
506
524
  readonly selection: SelectedTransition<States, E, R, Context>
525
+ readonly branchIndex: number
507
526
  readonly unresolvedTarget:
508
527
  | Machine.Snapshot<States>
509
528
  | Machine.Target<States, Machine.StateIdentifier<States>>
@@ -524,6 +543,7 @@ export type EvaluatedTransition<States extends Machine.StateSchemas, Event, E, R
524
543
  readonly source: string
525
544
  readonly trigger: Machine.TransitionTrigger
526
545
  readonly reenter: false
546
+ readonly branchIndex: number
527
547
  readonly target: string
528
548
  readonly resolvedTarget: string
529
549
  }>
@@ -1129,6 +1149,7 @@ interface ResolvedChoiceTransition {
1129
1149
  readonly source: string
1130
1150
  readonly trigger: Machine.TransitionTrigger
1131
1151
  readonly reenter: false
1152
+ readonly branchIndex: number
1132
1153
  readonly target: string
1133
1154
  readonly resolvedTarget: string
1134
1155
  }
@@ -1183,13 +1204,18 @@ function resolveChoiceTarget(
1183
1204
  history: configuration.history,
1184
1205
  ...(configuration.machineReferences === undefined ? {} : { machineReferences: configuration.machineReferences })
1185
1206
  }
1186
- const collected = collectTransition(machine, choice.transition, {
1187
- ...resolveMachineReferences(machine, provisional),
1188
- containingState: getParentValue(machine, provisional, node.path),
1189
- ancestors: getParentValues(machine, provisional, node.path),
1190
- event,
1191
- target: getTargetBuilder(machine, node.path)
1192
- })
1207
+ const collected = collectTransition(
1208
+ machine,
1209
+ choice.transition,
1210
+ {
1211
+ ...resolveMachineReferences(machine, provisional),
1212
+ containingState: getParentValue(machine, provisional, node.path),
1213
+ ancestors: getParentValues(machine, provisional, node.path),
1214
+ event,
1215
+ target: getTargetBuilder(machine, node.path)
1216
+ },
1217
+ choice.evaluate
1218
+ )
1193
1219
  if (collected.state === undefined) {
1194
1220
  throw new Error(`Machine choice resolver for "${node.path}" must return a target`)
1195
1221
  }
@@ -1201,6 +1227,7 @@ function resolveChoiceTarget(
1201
1227
  source: node.path,
1202
1228
  trigger: { type: "choice" },
1203
1229
  reenter: false,
1230
+ branchIndex: collected.branchIndex,
1204
1231
  target: returnedPath,
1205
1232
  resolvedTarget: nested?.target.path ?? returnedPath
1206
1233
  })
@@ -1240,7 +1267,8 @@ const collectEvaluatedTransition = <
1240
1267
  const transitionResult = collectTransition<States, Event, E, R, Context>(
1241
1268
  machine,
1242
1269
  selection.transition.transition,
1243
- selection.context
1270
+ selection.context,
1271
+ selection.transition.evaluate
1244
1272
  )
1245
1273
  const unresolvedTarget = transitionResult.state === undefined
1246
1274
  ? undefined
@@ -1369,6 +1397,7 @@ const collectEvaluatedTransition = <
1369
1397
  if (!changed) {
1370
1398
  return {
1371
1399
  selection,
1400
+ branchIndex: transitionResult.branchIndex,
1372
1401
  unresolvedTarget,
1373
1402
  target,
1374
1403
  commands: [
@@ -1414,6 +1443,7 @@ const collectEvaluatedTransition = <
1414
1443
 
1415
1444
  return {
1416
1445
  selection,
1446
+ branchIndex: transitionResult.branchIndex,
1417
1447
  unresolvedTarget,
1418
1448
  target,
1419
1449
  commands: [
@@ -1768,6 +1798,7 @@ const microstep = <
1768
1798
  source: transition.selection.sourcePath,
1769
1799
  trigger: transition.selection.trigger,
1770
1800
  reenter: transition.selection.transition.reenter,
1801
+ branchIndex: transition.branchIndex,
1771
1802
  target: transition.unresolvedTarget === undefined ? undefined : getTargetNodePath(transition.unresolvedTarget),
1772
1803
  resolvedTarget: transition.target === undefined ? undefined : getTargetNodePath(transition.target)
1773
1804
  },
@@ -25,6 +25,8 @@ export const ChoiceTargetTypeId: unique symbol = Symbol("effect/Machine/ChoiceTa
25
25
 
26
26
  export const NoTargetTypeId: unique symbol = Symbol("effect/Machine/NoTarget")
27
27
 
28
+ export const TargetSelectionTypeId: unique symbol = Symbol("effect/Machine/TargetSelection")
29
+
28
30
  interface StateInput {
29
31
  readonly [StateInputTypeId]: typeof StateInputTypeId
30
32
  readonly input: unknown
@@ -62,6 +64,32 @@ export interface NoTarget {
62
64
  readonly [NoTargetTypeId]: typeof NoTargetTypeId
63
65
  }
64
66
 
67
+ export type TargetSelectionKind = "state" | "initial" | "history" | "choice" | "none"
68
+
69
+ export type TargetSelectionScope = "local" | "branch" | "full" | "initial"
70
+
71
+ /** Immutable destination selected while a machine definition is captured. */
72
+ export interface TargetSelection {
73
+ readonly [TargetSelectionTypeId]: typeof TargetSelectionTypeId
74
+ readonly kind: TargetSelectionKind
75
+ readonly scope: TargetSelectionScope | undefined
76
+ readonly path: string | undefined
77
+ }
78
+
79
+ export const makeTargetSelection = (
80
+ kind: TargetSelectionKind,
81
+ path?: string,
82
+ scope?: TargetSelectionScope
83
+ ): TargetSelection =>
84
+ Object.freeze({
85
+ [TargetSelectionTypeId]: TargetSelectionTypeId as typeof TargetSelectionTypeId,
86
+ kind,
87
+ scope,
88
+ path
89
+ })
90
+
91
+ export const isTargetSelection = (u: unknown): u is TargetSelection => hasProperty(u, TargetSelectionTypeId)
92
+
65
93
  const noTarget = Object.freeze({
66
94
  [NoTargetTypeId]: NoTargetTypeId
67
95
  }) as NoTarget
@@ -381,12 +409,10 @@ export const compileStateNodes = (states: Machine.StateSchemas): Machine.StateNo
381
409
  } as Machine.StateNodes
382
410
  }
383
411
 
384
- const dynamicTransitionTargets = { type: "dynamic" } as const
385
-
386
- const transitionTargets = (handler: unknown): Machine.TransitionTargets =>
387
- typeof handler === "object" && handler !== null && "targets" in handler && handler.targets !== undefined
388
- ? { type: "declared", paths: Array.from(handler.targets as ReadonlyArray<string>) }
389
- : dynamicTransitionTargets
412
+ const transitionBranches = (handler: unknown): ReadonlyArray<Machine.TransitionBranch> =>
413
+ typeof handler === "object" && handler !== null && "branches" in handler && Array.isArray(handler.branches)
414
+ ? Array.from(handler.branches as ReadonlyArray<Machine.TransitionBranch>)
415
+ : []
390
416
 
391
417
  export const transitionDefinitions = (
392
418
  machine: Machine.Any
@@ -404,7 +430,7 @@ export const transitionDefinitions = (
404
430
  source: node.path,
405
431
  trigger: { type: "choice" },
406
432
  reenter: false,
407
- targets: transitionTargets(choice)
433
+ branches: transitionBranches(choice)
408
434
  })
409
435
  }
410
436
  continue
@@ -414,8 +440,8 @@ export const transitionDefinitions = (
414
440
  definitions.push({
415
441
  source: node.path,
416
442
  trigger: { type: "event", event },
417
- reenter: typeof handler === "object" && handler !== null && handler.reenter === true,
418
- targets: transitionTargets(handler)
443
+ reenter: hasProperty(handler, "reenter") && handler.reenter === true,
444
+ branches: transitionBranches(handler)
419
445
  })
420
446
  }
421
447
  if (config.always !== undefined) {
@@ -423,7 +449,7 @@ export const transitionDefinitions = (
423
449
  source: node.path,
424
450
  trigger: { type: "always" },
425
451
  reenter: false,
426
- targets: transitionTargets(config.always)
452
+ branches: transitionBranches(config.always)
427
453
  })
428
454
  }
429
455
  if (config.onDone !== undefined) {
@@ -431,7 +457,7 @@ export const transitionDefinitions = (
431
457
  source: node.path,
432
458
  trigger: { type: "done" },
433
459
  reenter: false,
434
- targets: transitionTargets(config.onDone)
460
+ branches: transitionBranches(config.onDone)
435
461
  })
436
462
  }
437
463
  const invokes = config.invoke === undefined
@@ -452,7 +478,7 @@ export const transitionDefinitions = (
452
478
  source: node.path,
453
479
  trigger: { type: "invoke", id, outcome },
454
480
  reenter: typeof handler === "object" && handler !== null && handler.reenter === true,
455
- targets: transitionTargets(handler)
481
+ branches: transitionBranches(handler)
456
482
  })
457
483
  }
458
484
  }
@@ -30,6 +30,7 @@ import type {
30
30
  import type { EnsureExecutable } from "../../machine/readiness.js"
31
31
  import { assertInvariants, type InvariantError } from "./invariant.js"
32
32
  import { appendTrace, run } from "./trace.js"
33
+ import { makeTransitionCoverageCollector } from "./transitionCoverage.js"
33
34
 
34
35
  type AnyMachine = Machine.Machine.Any
35
36
 
@@ -119,6 +120,10 @@ export const explore = <M extends AnyMachine, Key extends ExplorationKey>(
119
120
 
120
121
  const initialContext = stateContext(machine, initialTrace)
121
122
  const initialKey = validateKey(options.stateKey(initialContext))
123
+ const transitionCoverage = makeTransitionCoverageCollector<M>(machine)
124
+ for (const microstep of initialTrace.initial.plan.microsteps) {
125
+ transitionCoverage.observeMicrostep(microstep)
126
+ }
122
127
  const nodes: Array<ExplorationNode<M, Key>> = [{ ...initialContext, key: initialKey }]
123
128
  const nodeDraftsByKey = new Map<Key, number>([[initialKey, 0]])
124
129
  const edges: Array<ExplorationEdgeDraft<M>> = []
@@ -165,6 +170,8 @@ export const explore = <M extends AnyMachine, Key extends ExplorationKey>(
165
170
  const scenario = scenarioWithEvent(source.trace, event)
166
171
  const targetTrace = yield* appendTrace(machine, source.trace, event, scenario)
167
172
  plannedTransitions += 1
173
+ const step = targetTrace.steps[targetTrace.steps.length - 1]!
174
+ for (const microstep of step.plan.microsteps) transitionCoverage.observeMicrostep(microstep)
168
175
  if (invariants.length > 0) {
169
176
  yield* assertInvariants(machine, targetTrace, invariants)
170
177
  }
@@ -178,7 +185,7 @@ export const explore = <M extends AnyMachine, Key extends ExplorationKey>(
178
185
  target: existing,
179
186
  edge: {
180
187
  event,
181
- step: targetTrace.steps[targetTrace.steps.length - 1]!,
188
+ step,
182
189
  discovered: false
183
190
  }
184
191
  })
@@ -207,7 +214,7 @@ export const explore = <M extends AnyMachine, Key extends ExplorationKey>(
207
214
  target: targetIndex,
208
215
  edge: {
209
216
  event,
210
- step: targetTrace.steps[targetTrace.steps.length - 1]!,
217
+ step,
211
218
  discovered: true
212
219
  }
213
220
  })
@@ -243,6 +250,7 @@ export const explore = <M extends AnyMachine, Key extends ExplorationKey>(
243
250
  retainedEdges: edges.length,
244
251
  maxDepth: nodes.reduce((maximum, node) => Math.max(maximum, node.depth), 0)
245
252
  },
253
+ transitionCoverage: transitionCoverage.summary(),
246
254
  completeness
247
255
  }
248
256
  })
@@ -1177,44 +1177,6 @@ const stateValue = (
1177
1177
  return { _tag: stateTag(state.path), value: value ?? state.node.value }
1178
1178
  }
1179
1179
 
1180
- const runtimeTargetPath = (
1181
- byPath: ReadonlyMap<string, FlatFiniteState>,
1182
- sourcePath: string,
1183
- requestedPath: string
1184
- ): string => {
1185
- const source = byPath.get(sourcePath)!
1186
- const requested = byPath.get(requestedPath)!
1187
- if (requested.node._tag === "History") return requested.parent!
1188
- if (requested.node._tag === "Choice") return runtimeTargetPath(byPath, sourcePath, requested.node.selected)
1189
- if (source.root !== requested.root) return requested.path
1190
-
1191
- const initial = (path: string): string => {
1192
- const current = byPath.get(path)!
1193
- if (current.node._tag === "Parallel") {
1194
- if (sourcePath !== current.path && !sourcePath.startsWith(`${current.path}.`)) {
1195
- return current.path
1196
- }
1197
- const child = sourcePath === current.path
1198
- ? current.node.states[0]!.key
1199
- : sourcePath.slice(current.path.length + 1).split(".")[0]!
1200
- return initial(`${current.path}.${child}`)
1201
- }
1202
- return current.node._tag === "Compound" ? initial(`${current.path}.${current.node.initial}`) : current.path
1203
- }
1204
- const inspect = (path: string): string => {
1205
- const current = byPath.get(path)!
1206
- if (
1207
- current.node._tag === "Parallel" && sourcePath !== current.path && !sourcePath.startsWith(`${current.path}.`)
1208
- ) {
1209
- return current.path
1210
- }
1211
- if (current.path === requestedPath) return initial(current.path)
1212
- const next = requestedPath.slice(current.path.length + 1).split(".")[0]!
1213
- return inspect(`${current.path}.${next}`)
1214
- }
1215
- return inspect(source.root)
1216
- }
1217
-
1218
1180
  const makeStateTree = (
1219
1181
  states: ReadonlyArray<FiniteState>,
1220
1182
  parent: string | undefined
@@ -1357,10 +1319,88 @@ const selectHistoryTarget = (builder: Record<string, any>, path: string): unknow
1357
1319
  return current[parts[parts.length - 1]!]()
1358
1320
  }
1359
1321
 
1360
- type TargetBuilder = {
1361
- readonly branch: Record<string, any>
1362
- readonly full: Record<string, any>
1363
- readonly history: Record<string, any>
1322
+ const selectableDefinitionTarget = (
1323
+ source: string,
1324
+ target: string,
1325
+ byPath: ReadonlyMap<string, FlatFiniteState>
1326
+ ): string => {
1327
+ const parts = target.split(".")
1328
+ for (let index = 0; index < parts.length; index++) {
1329
+ const path = parts.slice(0, index + 1).join(".")
1330
+ const node = byPath.get(path)?.node
1331
+ if (
1332
+ (node?._tag === "Compound" || node?._tag === "Parallel") &&
1333
+ source !== path && !source.startsWith(`${path}.`)
1334
+ ) return path
1335
+ }
1336
+ return target
1337
+ }
1338
+
1339
+ const selectDefinitionTarget = (
1340
+ selector: Record<string, any>,
1341
+ source: string,
1342
+ target: string,
1343
+ byPath: ReadonlyMap<string, FlatFiniteState>
1344
+ ): unknown => {
1345
+ const selected = byPath.get(target)!
1346
+ if (selected.node._tag === "History") {
1347
+ return selectHistoryTarget(selector.history, target)
1348
+ }
1349
+ const sourceRoot = byPath.get(source)!.root
1350
+ if (selected.root !== sourceRoot) {
1351
+ const root = target.split(".")[0]!
1352
+ return selector.full[root]()
1353
+ }
1354
+ const selectable = selectableDefinitionTarget(source, target, byPath)
1355
+ let current = selector.branch
1356
+ const parts = selectable.split(".")
1357
+ for (const part of parts) current = current[part]
1358
+ if (typeof current === "function") return current()
1359
+ return current.initial()
1360
+ }
1361
+
1362
+ const resolveDefinitionTarget = (
1363
+ builder: any,
1364
+ source: string,
1365
+ target: string,
1366
+ byPath: ReadonlyMap<string, FlatFiniteState>,
1367
+ value?: number
1368
+ ): unknown => {
1369
+ const selected = byPath.get(target)!
1370
+ if (selected.node._tag === "History") return builder()
1371
+ if (selected.root !== byPath.get(source)!.root) {
1372
+ const parts = target.split(".")
1373
+ return selectSnapshot({ [parts[0]!]: builder }, parts[0]!, byPath, parts, 0, source, value)
1374
+ }
1375
+ const selectable = selectableDefinitionTarget(source, target, byPath)
1376
+ if (selectable !== target) {
1377
+ const bound = byPath.get(selectable)!
1378
+ const parts = target.split(".")
1379
+ return selectSnapshot(
1380
+ { [bound.node.key]: builder },
1381
+ selectable,
1382
+ byPath,
1383
+ parts,
1384
+ selectable.split(".").length - 1,
1385
+ source,
1386
+ value
1387
+ )
1388
+ }
1389
+ if (selected.node._tag === "Choice") return builder()
1390
+ const input = { value: value ?? selected.node.value }
1391
+ if (selected.node._tag === "Compound" || selected.node._tag === "Parallel") {
1392
+ const parts = target.split(".")
1393
+ return selectSnapshot(
1394
+ { [selected.node.key]: builder },
1395
+ target,
1396
+ byPath,
1397
+ parts,
1398
+ parts.length - 1,
1399
+ source,
1400
+ value
1401
+ )
1402
+ }
1403
+ return builder.from(input)
1364
1404
  }
1365
1405
 
1366
1406
  const makeHandlers = (
@@ -1375,15 +1415,11 @@ const makeHandlers = (
1375
1415
  if (node._tag === "History") continue
1376
1416
  if (node._tag === "Choice") {
1377
1417
  handlers[node.key] = {
1378
- choice: {
1379
- targets: node.targets,
1380
- transition: ({ target }: { readonly target: TargetBuilder }) => {
1381
- const selected = byPath.get(node.selected)!
1382
- const parts = selected.path.split(".")
1383
- const builder = selected.root === byPath.get(path)!.root ? target.branch : target.full
1384
- return selectSnapshot(builder, parts[0]!, byPath, parts, 0, path)
1385
- }
1386
- }
1418
+ choice: (Machine.transition as any)({
1419
+ target: (to: Record<string, any>) => selectDefinitionTarget(to, path, node.selected, byPath) as any,
1420
+ resolve: ({ target }: { readonly target: any }) =>
1421
+ resolveDefinitionTarget(target, path, node.selected, byPath)
1422
+ } as any)
1387
1423
  }
1388
1424
  continue
1389
1425
  }
@@ -1396,27 +1432,17 @@ const makeHandlers = (
1396
1432
  let onDone: unknown
1397
1433
  for (const transition of transitions.values()) {
1398
1434
  if (transition.source !== path) continue
1399
- const config = {
1435
+ const config = (Machine.transition as any)({
1400
1436
  ...("reenter" in transition ? { reenter: transition.reenter } : {}),
1401
- ...(transition.target === undefined
1402
- ? {}
1403
- : {
1404
- targets: [
1405
- byPath.get(transition.target)!.node._tag === "History" ||
1406
- byPath.get(transition.target)!.node._tag === "Choice"
1407
- ? transition.target
1408
- : runtimeTargetPath(byPath, path, transition.target)
1409
- ]
1410
- }),
1411
- transition: ({ target }: { readonly target: TargetBuilder }) => {
1412
- if (transition.target === undefined) return undefined
1413
- const targetState = byPath.get(transition.target)!
1414
- if (targetState.node._tag === "History") return selectHistoryTarget(target.history, targetState.path)
1415
- const parts = transition.target.split(".")
1416
- const builder = targetState.root === byPath.get(path)!.root ? target.branch : target.full
1417
- return selectSnapshot(builder, parts[0]!, byPath, parts, 0, path, transition.targetValue)
1418
- }
1419
- }
1437
+ target: (to: Record<string, any>) =>
1438
+ transition.target === undefined
1439
+ ? to.none()
1440
+ : selectDefinitionTarget(to, path, transition.target, byPath),
1441
+ resolve: transition.target === undefined
1442
+ ? () => undefined
1443
+ : ({ target }: { readonly target: any }) =>
1444
+ resolveDefinitionTarget(target, path, transition.target!, byPath, transition.targetValue)
1445
+ } as any)
1420
1446
  if (transition.trigger.type === "event") on[transition.trigger.event] = config
1421
1447
  else if (transition.trigger.type === "always") always = config
1422
1448
  else onDone = config
@@ -1489,8 +1515,15 @@ export const compileModel = (model: FiniteModel): Machine.Machine.Any => {
1489
1515
  const machine = Machine.make({
1490
1516
  states: defined.states as any,
1491
1517
  events: Machine.events(...eventSchemas) as any,
1492
- initial: () => selectSnapshot(defined.initial as any, initial.path, byPath, [initial.path], 0) as any
1493
- })
1518
+ initial: {
1519
+ target: (to: Record<string, any>) => {
1520
+ const selected = to[initial.path]
1521
+ return typeof selected === "function" ? selected() : selected.initial()
1522
+ },
1523
+ resolve: ({ target }: { readonly target: any }) =>
1524
+ selectSnapshot({ [initial.path]: target }, initial.path, byPath, [initial.path], 0) as any
1525
+ }
1526
+ } as any)
1494
1527
  const transitions = new Map(model.transitions.map((transition) => [
1495
1528
  `${transition.source}\u0000${triggerKey(transition.trigger)}`,
1496
1529
  transition
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Exact transition-definition and branch coverage collected from retained
3
+ * planner microsteps.
4
+ *
5
+ * @since 0.14.0
6
+ */
7
+
8
+ import * as Machine from "../../../Machine.js"
9
+ import type {
10
+ CoverageSummary,
11
+ Microstep,
12
+ TransitionBranchCoverageItem,
13
+ TransitionCoverage,
14
+ TransitionDefinitionCoverageItem
15
+ } from "../../../testing/MachineTest.js"
16
+
17
+ type AnyMachine = Machine.Machine.Any
18
+
19
+ type StateNodePath<M extends AnyMachine> = Machine.Machine.StateNodeIdentifier<Machine.Machine.States<M>>
20
+
21
+ type EventTag<M extends AnyMachine> = Machine.Machine.TagOf<Machine.Machine.Events<M>[number]>
22
+
23
+ type MachineTransitionCoverage<M extends AnyMachine> = TransitionCoverage<
24
+ StateNodePath<M>,
25
+ EventTag<M>,
26
+ StateNodePath<M>
27
+ >
28
+
29
+ const coverageSummary = <Item>(declared: ReadonlyArray<Item>, hit: ReadonlySet<number>): CoverageSummary<Item> => {
30
+ const hits: Array<Item> = []
31
+ const misses: Array<Item> = []
32
+ declared.forEach((item, index) => (hit.has(index) ? hits : misses).push(item))
33
+ return {
34
+ total: declared.length,
35
+ hit: hits.length,
36
+ missing: misses.length,
37
+ hits,
38
+ misses
39
+ }
40
+ }
41
+
42
+ export const sameTransitionTrigger = (
43
+ left: Machine.Machine.TransitionTrigger,
44
+ right: Machine.Machine.TransitionTrigger
45
+ ): boolean => {
46
+ if (left.type !== right.type) return false
47
+ if (left.type === "event") return right.type === "event" && left.event === right.event
48
+ if (left.type === "invoke") {
49
+ return right.type === "invoke" && left.id === right.id && left.outcome === right.outcome
50
+ }
51
+ return true
52
+ }
53
+
54
+ export interface TransitionCoverageCollector<M extends AnyMachine> {
55
+ readonly observeMicrostep: (microstep: Microstep<M, unknown>) => void
56
+ readonly summary: () => MachineTransitionCoverage<M>
57
+ }
58
+
59
+ export const makeTransitionCoverageCollector = <M extends AnyMachine>(
60
+ machine: M
61
+ ): TransitionCoverageCollector<M> => {
62
+ const definitions = Machine.transitionDefinitions(machine).map(
63
+ (definition, index): TransitionDefinitionCoverageItem<StateNodePath<M>, EventTag<M>, StateNodePath<M>> => ({
64
+ id: `transition:${index}`,
65
+ index,
66
+ source: definition.source,
67
+ trigger: definition.trigger,
68
+ reenter: definition.reenter,
69
+ branches: definition.branches
70
+ })
71
+ )
72
+ const definitionHits = new Set<number>()
73
+ const branchOffsets: Array<number> = []
74
+ const branches: Array<TransitionBranchCoverageItem<StateNodePath<M>, EventTag<M>, StateNodePath<M>>> = []
75
+ for (let definitionIndex = 0; definitionIndex < definitions.length; definitionIndex++) {
76
+ branchOffsets.push(branches.length)
77
+ const definition = definitions[definitionIndex]!
78
+ definition.branches.forEach((branch, branchIndex) => {
79
+ branches.push({
80
+ id: `transition:${definitionIndex}:branch:${branchIndex}`,
81
+ definitionIndex,
82
+ branchIndex,
83
+ source: definition.source,
84
+ trigger: definition.trigger,
85
+ reenter: definition.reenter,
86
+ branch
87
+ })
88
+ })
89
+ }
90
+ const branchHits = new Set<number>()
91
+
92
+ return {
93
+ observeMicrostep(microstep) {
94
+ for (const retained of microstep.transitions) {
95
+ const definitionIndex = definitions.findIndex((definition) =>
96
+ definition.source === retained.source &&
97
+ definition.reenter === retained.reenter &&
98
+ sameTransitionTrigger(definition.trigger, retained.trigger)
99
+ )
100
+ if (definitionIndex === -1) continue
101
+ definitionHits.add(definitionIndex)
102
+ const definition = definitions[definitionIndex]!
103
+ if (
104
+ Number.isSafeInteger(retained.branchIndex) && retained.branchIndex >= 0 &&
105
+ retained.branchIndex < definition.branches.length
106
+ ) {
107
+ branchHits.add(branchOffsets[definitionIndex]! + retained.branchIndex)
108
+ }
109
+ }
110
+ },
111
+ summary: () => ({
112
+ definitions: coverageSummary(definitions, definitionHits),
113
+ branches: coverageSummary(branches, branchHits)
114
+ })
115
+ }
116
+ }