@typeonce/effect-machine 0.13.0 → 0.14.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 (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 +306 -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 +407 -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
@@ -29,7 +29,6 @@ import type {
29
29
  StateCoverageItem,
30
30
  Trace,
31
31
  TraceStep,
32
- TransitionCoverageItem,
33
32
  VerificationLaw,
34
33
  VerificationLawGroup,
35
34
  VerificationViolation,
@@ -40,6 +39,7 @@ import { toArbitraryWithReport } from "./arbitrary.js"
40
39
  import type { FiniteModel } from "./finiteModel.js"
41
40
  import * as ReferenceModel from "./referenceModel.js"
42
41
  import { rawConfigurationPaths, run } from "./trace.js"
42
+ import { makeTransitionCoverageCollector, sameTransitionTrigger } from "./transitionCoverage.js"
43
43
 
44
44
  export {
45
45
  advanceCommand,
@@ -469,18 +469,21 @@ const normalizeTraces = <M extends AnyMachine>(
469
469
  ): ReadonlyArray<Trace<M>> =>
470
470
  Array.isArray(traceOrTraces) ? traceOrTraces as ReadonlyArray<Trace<M>> : [traceOrTraces as Trace<M>]
471
471
 
472
- const sameCoverageTrigger = (
473
- left: Machine.Machine.TransitionTrigger,
474
- right: Machine.Machine.TransitionTrigger
475
- ): boolean =>
476
- left.type === right.type && (left.type !== "event" || right.type === "event" && left.event === right.event)
477
-
478
- const targetWithinDeclaredBounds = (
472
+ const targetWithinSelection = (
479
473
  target: string | undefined,
480
- bounds: Machine.Machine.TransitionTargets
481
- ): boolean =>
482
- target === undefined || bounds.type === "dynamic" ||
483
- bounds.paths.some((path) => target === path || target.startsWith(`${path}.`))
474
+ branch: Machine.Machine.TransitionBranch,
475
+ nodeByPath: ReadonlyMap<string, Machine.Machine.StateNode>
476
+ ): boolean => {
477
+ const selection = branch.selection
478
+ if (selection.kind === "none") return target === undefined
479
+ if (target === undefined || selection.path === undefined) return false
480
+ if (target === selection.path) return true
481
+ const selectedNode = nodeByPath.get(selection.path)
482
+ return selection.kind === "state" &&
483
+ (selection.scope === "local" || selection.scope === "branch") &&
484
+ (selectedNode?.type === "compound" || selectedNode?.type === "parallel") &&
485
+ target.startsWith(`${selection.path}.`)
486
+ }
484
487
 
485
488
  const finiteTagValues = (ast: SchemaAST.AST): ReadonlyArray<PropertyKey> | undefined => {
486
489
  if (SchemaAST.isLiteral(ast)) {
@@ -560,24 +563,7 @@ export const coverage = <M extends AnyMachine>(
560
563
  const entryHits = new Set<number>()
561
564
  const exitHits = new Set<number>()
562
565
 
563
- const definitions = Machine.transitionDefinitions(machine).map(
564
- (
565
- definition,
566
- index
567
- ): TransitionCoverageItem<
568
- StateNodePath<M>,
569
- Machine.Machine.TagOf<Machine.Machine.Events<M>[number]>,
570
- StateNodePath<M>
571
- > => ({
572
- id: `transition:${index}:${formatValue(definition)}`,
573
- index,
574
- source: definition.source,
575
- trigger: definition.trigger,
576
- reenter: definition.reenter,
577
- targets: definition.targets
578
- })
579
- )
580
- const transitionHits = new Set<number>()
566
+ const transitionCoverage = makeTransitionCoverageCollector(machine)
581
567
 
582
568
  const declaredEvents = publicEventTags(machine)
583
569
  const declaredEventTags = declaredEvents.tags
@@ -635,6 +621,7 @@ export const coverage = <M extends AnyMachine>(
635
621
  }
636
622
 
637
623
  const observeMicrostep = (microstep: Microstep<M, any>): void => {
624
+ transitionCoverage.observeMicrostep(microstep)
638
625
  microsteps += 1
639
626
  if (microstep.changed) changedMicrosteps += 1
640
627
  raisedEvents += microstep.raisedEvents.length
@@ -652,13 +639,6 @@ export const coverage = <M extends AnyMachine>(
652
639
  historyTargets += 1
653
640
  if (retained.resolvedTarget !== undefined) resolvedHistoryTargets += 1
654
641
  }
655
- const definitionIndex = definitions.findIndex((definition) =>
656
- definition.source === retained.source &&
657
- definition.reenter === retained.reenter &&
658
- sameCoverageTrigger(definition.trigger, retained.trigger) &&
659
- targetWithinDeclaredBounds(retained.target, definition.targets)
660
- )
661
- if (definitionIndex !== -1) transitionHits.add(definitionIndex)
662
642
  }
663
643
  }
664
644
 
@@ -698,7 +678,7 @@ export const coverage = <M extends AnyMachine>(
698
678
  entry: coverageSummary(activeNodes, entryHits),
699
679
  exit: coverageSummary(activeNodes, exitHits)
700
680
  },
701
- transitions: coverageSummary(definitions, transitionHits),
681
+ transitions: transitionCoverage.summary(),
702
682
  events: declaredEvents.diagnostics.length === 0
703
683
  ? {
704
684
  available: true,
@@ -966,12 +946,6 @@ const sameValue = (left: unknown, right: unknown): boolean => formatValue(left)
966
946
  const samePaths = (left: ReadonlyArray<string>, right: ReadonlyArray<string>): boolean =>
967
947
  left.length === right.length && left.every((path, index) => path === right[index])
968
948
 
969
- const sameTrigger = (
970
- left: Machine.Machine.TransitionTrigger,
971
- right: Machine.Machine.TransitionTrigger
972
- ): boolean =>
973
- left.type === right.type && (left.type !== "event" || right.type === "event" && left.event === right.event)
974
-
975
949
  const makeNodeUtilities = (nodes: ReadonlyArray<PublicStateNode>) => {
976
950
  const byPath = new Map(nodes.map((node) => [node.path, node]))
977
951
  const depth = (path: string): number => {
@@ -1015,9 +989,10 @@ export const verify = <M extends AnyMachine>(
1015
989
  options: VerifyOptions = {}
1016
990
  ): Effect.Effect<void, VerificationError> => {
1017
991
  const selected = new Set<VerificationLawGroup>(
1018
- options.laws ?? ["configuration", "microsteps", "completion", "history", "targetBounds"]
992
+ options.laws ?? ["configuration", "microsteps", "completion", "history", "definitions"]
1019
993
  )
1020
994
  const nodes = Machine.stateNodes(machine) as ReadonlyArray<PublicStateNode>
995
+ const initialDefinition = Machine.initialDefinition(machine)
1021
996
  const definitions = Machine.transitionDefinitions(machine)
1022
997
  const { ancestors, byPath, depth, isDescendantOrSelf } = makeNodeUtilities(nodes)
1023
998
  const violations: Array<VerificationViolation> = []
@@ -1493,34 +1468,197 @@ export const verify = <M extends AnyMachine>(
1493
1468
  return direction === "entry" ? leftOrder - rightOrder : rightOrder - leftOrder
1494
1469
  })
1495
1470
 
1496
- const validateTransitionBounds = (
1471
+ const transitionBranch = (
1472
+ transition: Microstep<M>["transitions"][number]
1473
+ ): Machine.Machine.TransitionBranch | undefined => {
1474
+ const definition = definitions.find((candidate) =>
1475
+ candidate.source === transition.source && candidate.reenter === transition.reenter &&
1476
+ sameTransitionTrigger(candidate.trigger, transition.trigger)
1477
+ )
1478
+ return definition === undefined || !Number.isSafeInteger(transition.branchIndex) || transition.branchIndex < 0 ||
1479
+ transition.branchIndex >= definition.branches.length
1480
+ ? undefined
1481
+ : definition.branches[transition.branchIndex]
1482
+ }
1483
+
1484
+ const validateTransitionDefinition = (
1497
1485
  transition: Microstep<M>["transitions"][number],
1498
1486
  location: VerificationLocation
1499
- ): void => {
1500
- if (!selected.has("targetBounds")) return
1487
+ ): Machine.Machine.TransitionBranch | undefined => {
1488
+ if (!selected.has("definitions")) return undefined
1501
1489
  const definition = definitions.find((candidate) =>
1502
1490
  candidate.source === transition.source && candidate.reenter === transition.reenter &&
1503
- sameTrigger(candidate.trigger, transition.trigger)
1491
+ sameTransitionTrigger(candidate.trigger, transition.trigger)
1504
1492
  )
1505
1493
  if (definition === undefined) {
1506
1494
  add(
1507
- "targetBounds.definition",
1495
+ "definitions.transition",
1508
1496
  location,
1509
1497
  `retained transition from "${transition.source}" has no public definition`,
1510
1498
  transition.source
1511
1499
  )
1512
- return
1500
+ return undefined
1501
+ }
1502
+ if (
1503
+ !Number.isSafeInteger(transition.branchIndex) || transition.branchIndex < 0 ||
1504
+ transition.branchIndex >= definition.branches.length
1505
+ ) {
1506
+ add(
1507
+ "definitions.branchIndex",
1508
+ location,
1509
+ `retained transition from "${transition.source}" selected invalid branch index ${transition.branchIndex}`,
1510
+ transition.source
1511
+ )
1512
+ return undefined
1513
1513
  }
1514
- if (transition.target === undefined || definition.targets.type === "dynamic") return
1515
- if (!definition.targets.paths.some((bound) => isDescendantOrSelf(String(transition.target), String(bound)))) {
1514
+ const branch = definition.branches[transition.branchIndex]!
1515
+ if (!targetWithinSelection(transition.target, branch, byPath)) {
1516
+ const expected = branch.selection.kind === "none"
1517
+ ? "an explicitly targetless result"
1518
+ : `selection ${branch.selection.kind}:${branch.selection.scope}:${String(branch.selection.path)}`
1516
1519
  add(
1517
- "targetBounds.target",
1520
+ "definitions.selection",
1518
1521
  location,
1519
- `transition target "${String(transition.target)}" is outside declared bounds ` +
1520
- `[${definition.targets.paths.join(", ")}]`,
1521
- String(transition.target)
1522
+ `transition branch ${transition.branchIndex} from "${transition.source}" returned ` +
1523
+ `target "${String(transition.target)}" outside ${expected}`,
1524
+ transition.target === undefined ? transition.source : String(transition.target)
1522
1525
  )
1523
1526
  }
1527
+ return branch
1528
+ }
1529
+
1530
+ const validateTransitionResolutions = (
1531
+ transitions: Microstep<M>["transitions"],
1532
+ branches: ReadonlyArray<Machine.Machine.TransitionBranch | undefined>,
1533
+ location: VerificationLocation
1534
+ ): void => {
1535
+ if (!selected.has("definitions")) return
1536
+
1537
+ const choiceResolution = (
1538
+ start: string,
1539
+ afterIndex: number,
1540
+ nested: boolean
1541
+ ): { readonly found: boolean; readonly target: string | undefined } => {
1542
+ const seen = new Set<string>()
1543
+ let choice = start
1544
+ let cursor = afterIndex + 1
1545
+ let first = true
1546
+ while (!seen.has(choice)) {
1547
+ seen.add(choice)
1548
+ let choiceIndex = -1
1549
+ for (let index = cursor; index < transitions.length; index++) {
1550
+ const candidate = transitions[index]!
1551
+ if (
1552
+ candidate.trigger.type === "choice" &&
1553
+ (candidate.source === choice || first && nested && isDescendantOrSelf(String(candidate.source), choice))
1554
+ ) {
1555
+ choiceIndex = index
1556
+ break
1557
+ }
1558
+ }
1559
+ if (choiceIndex === -1) return { found: false, target: undefined }
1560
+ const transition = transitions[choiceIndex]!
1561
+ if (branches[choiceIndex] === undefined) return { found: false, target: undefined }
1562
+ const target = transition.target === undefined ? undefined : String(transition.target)
1563
+ if (target === undefined) return { found: true, target: undefined }
1564
+ const targetNode = byPath.get(target)
1565
+ if (targetNode?.type === "choice") {
1566
+ choice = target
1567
+ cursor = choiceIndex + 1
1568
+ first = false
1569
+ continue
1570
+ }
1571
+ return {
1572
+ found: true,
1573
+ target: targetNode?.type === "history" ? targetNode.parent : target
1574
+ }
1575
+ }
1576
+ return { found: false, target: undefined }
1577
+ }
1578
+
1579
+ transitions.forEach((transition, index) => {
1580
+ if (branches[index] === undefined) return
1581
+ const target = transition.target === undefined ? undefined : String(transition.target)
1582
+ const resolvedTarget = transition.resolvedTarget === undefined ? undefined : String(transition.resolvedTarget)
1583
+ const targetNode = target === undefined ? undefined : byPath.get(target)
1584
+ let expected = target
1585
+ let explanation = target === undefined ? "an unresolved targetless transition" : `target "${target}"`
1586
+
1587
+ if (transition.trigger.type === "choice") {
1588
+ explanation = target === undefined ? "a targetless choice edge" : `choice edge target "${target}"`
1589
+ } else if (targetNode?.type === "history") {
1590
+ expected = targetNode.parent
1591
+ explanation = `history owner "${String(targetNode.parent)}"`
1592
+ } else if (targetNode?.type === "choice") {
1593
+ const resolution = choiceResolution(targetNode.path, index, false)
1594
+ if (!resolution.found) {
1595
+ add(
1596
+ "definitions.resolution",
1597
+ location,
1598
+ `transition from "${transition.source}" targets choice "${target}" without an exact retained route`,
1599
+ target
1600
+ )
1601
+ return
1602
+ }
1603
+ expected = resolution.target
1604
+ explanation = expected === undefined ?
1605
+ `targetless route from choice "${target}"` :
1606
+ `choice route from "${target}" to "${expected}"`
1607
+ } else if (
1608
+ resolvedTarget !== target &&
1609
+ (targetNode?.type === "compound" || targetNode?.type === "parallel")
1610
+ ) {
1611
+ const resolution = choiceResolution(targetNode.path, index, true)
1612
+ if (resolution.found) {
1613
+ expected = resolution.target
1614
+ explanation = expected === undefined ?
1615
+ `targetless nested choice route from "${target}"` :
1616
+ `nested choice route from "${target}" to "${expected}"`
1617
+ }
1618
+ }
1619
+
1620
+ if (resolvedTarget !== expected) {
1621
+ add(
1622
+ "definitions.resolution",
1623
+ location,
1624
+ `transition branch ${transition.branchIndex} from "${transition.source}" resolved to ` +
1625
+ `"${String(transition.resolvedTarget)}" instead of ${explanation}`,
1626
+ resolvedTarget ?? target ?? String(transition.source)
1627
+ )
1628
+ }
1629
+ })
1630
+ }
1631
+
1632
+ const initialChoiceRouteRoot = (): string | undefined => {
1633
+ const routing = trace.initial.plan.microsteps[0]
1634
+ if (
1635
+ routing === undefined || routing.changed ||
1636
+ routing.transitions.length === 0 ||
1637
+ !routing.transitions.every((transition) => transition.trigger.type === "choice")
1638
+ ) {
1639
+ return undefined
1640
+ }
1641
+
1642
+ const reachableScopes: Array<string> = [initialDefinition.target]
1643
+ let terminalRoot: string | undefined
1644
+ for (const transition of routing.transitions) {
1645
+ const source = String(transition.source)
1646
+ const branch = transitionBranch(transition)
1647
+ if (
1648
+ branch === undefined || !targetWithinSelection(transition.target, branch, byPath) ||
1649
+ !reachableScopes.some((scope) => isDescendantOrSelf(source, scope))
1650
+ ) {
1651
+ return undefined
1652
+ }
1653
+ const target = transition.target === undefined ? undefined : String(transition.target)
1654
+ if (target === undefined) return undefined
1655
+ const targetNode = byPath.get(target)
1656
+ const scope = targetNode?.type === "history" ? targetNode.parent : target
1657
+ if (scope === undefined) return undefined
1658
+ reachableScopes.push(scope)
1659
+ terminalRoot = ancestors(scope)[0]
1660
+ }
1661
+ return terminalRoot
1524
1662
  }
1525
1663
 
1526
1664
  const validateMicrostep = (
@@ -1675,7 +1813,8 @@ export const verify = <M extends AnyMachine>(
1675
1813
  }
1676
1814
  }
1677
1815
  }
1678
- for (const transition of microstep.transitions) validateTransitionBounds(transition, location)
1816
+ const branches = microstep.transitions.map((transition) => validateTransitionDefinition(transition, location))
1817
+ validateTransitionResolutions(microstep.transitions, branches, location)
1679
1818
  }
1680
1819
 
1681
1820
  const validatePlanCompletion = (
@@ -1711,6 +1850,24 @@ export const verify = <M extends AnyMachine>(
1711
1850
 
1712
1851
  const initialLocation: VerificationLocation = { eventIndex: undefined }
1713
1852
  const starting = inspectSnapshot(trace.initial.startingState, initialLocation, "initial starting state")
1853
+ if (selected.has("definitions")) {
1854
+ const startingRoots = starting.paths.filter((path) => byPath.get(path)?.parent === undefined)
1855
+ const routedRoot = startingRoots.length === 1 && startingRoots[0] !== initialDefinition.target
1856
+ ? initialChoiceRouteRoot()
1857
+ : undefined
1858
+ if (
1859
+ startingRoots.length !== 1 ||
1860
+ startingRoots[0] !== initialDefinition.target && startingRoots[0] !== routedRoot
1861
+ ) {
1862
+ add(
1863
+ "definitions.initial",
1864
+ initialLocation,
1865
+ `initial starting state selected roots [${startingRoots.join(", ")}] without an exact route from ` +
1866
+ `declared root "${initialDefinition.target}"`,
1867
+ startingRoots[0] ?? initialDefinition.target
1868
+ )
1869
+ }
1870
+ }
1714
1871
  validateSnapshotMetadata(starting, initialLocation, "initial starting state")
1715
1872
  validateTraceConfiguration(
1716
1873
  starting,
@@ -1845,6 +2002,7 @@ const formatMicrosteps = <M extends AnyMachine>(microsteps: ReadonlyArray<Micros
1845
2002
  source: transition.source,
1846
2003
  trigger: transition.trigger,
1847
2004
  reenter: transition.reenter,
2005
+ branchIndex: transition.branchIndex,
1848
2006
  target: transition.target,
1849
2007
  resolvedTarget: transition.resolvedTarget
1850
2008
  }))
@@ -231,8 +231,20 @@ export interface Scenarios<M extends AnyMachine> {
231
231
  * const machine = Machine.make({
232
232
  * states: States.states,
233
233
  * events: Machine.events(Reset),
234
- * initial: () => States.initial.Idle.from()
235
- * }).handle({ Idle: { on: { Reset: () => States.initial.Idle.from() } } })
234
+ * initial: {
235
+ * target: (to) => to.Idle(),
236
+ * resolve: ({ target }) => target.from()
237
+ * }
238
+ * }).handle({
239
+ * Idle: {
240
+ * on: {
241
+ * Reset: Machine.transition({
242
+ * target: (to) => to.full.Idle(),
243
+ * resolve: ({ target }) => target.from()
244
+ * })
245
+ * }
246
+ * }
247
+ * })
236
248
  *
237
249
  * const generated = MachineTest.scenarios(machine, { maxEvents: 5 })
238
250
  * ```
@@ -480,7 +492,10 @@ export { ProbeUnavailableError } from "../internal/testing/machine/verification.
480
492
  * const machine = Machine.make({
481
493
  * states: States.states,
482
494
  * events: Machine.events(),
483
- * initial: () => States.initial.Idle.from()
495
+ * initial: {
496
+ * target: (to) => to.Idle(),
497
+ * resolve: ({ target }) => target.from()
498
+ * }
484
499
  * }).handle({ Idle: {} })
485
500
  *
486
501
  * const program = Effect.gen(function*() {
@@ -1090,7 +1105,10 @@ export const Invariant: {
1090
1105
  * const machine = Machine.make({
1091
1106
  * states: States.states,
1092
1107
  * events: Machine.events(),
1093
- * initial: () => States.initial.Count(new Count({ value: 0 }))
1108
+ * initial: {
1109
+ * target: (to) => to.Count(),
1110
+ * resolve: ({ target }) => target(new Count({ value: 0 }))
1111
+ * }
1094
1112
  * }).handle({ Count: {} })
1095
1113
  *
1096
1114
  * const nonNegative = MachineTest.invariants(machine).state(
@@ -1357,6 +1375,17 @@ export interface Exploration<M extends AnyMachine, Key extends ExplorationKey =
1357
1375
  readonly start: Graph.NodeIndex
1358
1376
  readonly limits: ResolvedExplorationLimits
1359
1377
  readonly stats: ExplorationStats
1378
+ /**
1379
+ * Exact declared transition branches witnessed by concretely planned work.
1380
+ * This is observed evidence, not a claim that every reachable branch was
1381
+ * explored. Startup and state-limit plans count; unplanned depth- and
1382
+ * transition-limit frontiers do not.
1383
+ */
1384
+ readonly transitionCoverage: TransitionCoverage<
1385
+ StateNodePath<M>,
1386
+ Machine.Machine.TagOf<Machine.Machine.Events<M>[number]>,
1387
+ StateNodePath<M>
1388
+ >
1360
1389
  readonly completeness: ExplorationCompleteness<M, Key>
1361
1390
  }
1362
1391
 
@@ -1391,7 +1420,10 @@ export type ExploreOptions<M extends AnyMachine, Key extends ExplorationKey = Ex
1391
1420
  *
1392
1421
  * Invariants are checked against startup and every concretely planned edge,
1393
1422
  * so a failure retains a shortest discovered counterexample. Staged actions
1394
- * and runtime activities are not executed.
1423
+ * and runtime activities are not executed. `transitionCoverage` includes
1424
+ * startup and every event plan that was actually computed, including a plan
1425
+ * retained at a state-limit frontier. Depth- and transition-limit frontiers
1426
+ * have no plan and therefore contribute no transition hits.
1395
1427
  *
1396
1428
  * **Example**
1397
1429
  *
@@ -1408,10 +1440,19 @@ export type ExploreOptions<M extends AnyMachine, Key extends ExplorationKey = Ex
1408
1440
  * const machine = Machine.make({
1409
1441
  * states: States.states,
1410
1442
  * events: Machine.events(Increment),
1411
- * initial: () => States.initial.Count(new Count({ value: 0 }))
1443
+ * initial: {
1444
+ * target: (to) => to.Count(),
1445
+ * resolve: ({ target }) => target(new Count({ value: 0 }))
1446
+ * }
1412
1447
  * }).handle({
1413
- * Count: { on: { Increment: ({ state }) =>
1414
- * States.initial.Count(new Count({ value: state.value + 1 })) } }
1448
+ * Count: {
1449
+ * on: {
1450
+ * Increment: Machine.transition({
1451
+ * target: (to) => to.full.Count(),
1452
+ * resolve: ({ state, target }) => target(new Count({ value: state.value + 1 }))
1453
+ * })
1454
+ * }
1455
+ * }
1415
1456
  * })
1416
1457
  *
1417
1458
  * const explored = MachineTest.explore(machine, {
@@ -1592,7 +1633,10 @@ export type RunServices<M extends AnyMachine> = IsAny<
1592
1633
  * const machine = Machine.make({
1593
1634
  * states: States.states,
1594
1635
  * events: Machine.events(),
1595
- * initial: () => States.initial.Idle.from()
1636
+ * initial: {
1637
+ * target: (to) => to.Idle(),
1638
+ * resolve: ({ target }) => target.from()
1639
+ * }
1596
1640
  * }).handle({ Idle: {} })
1597
1641
  *
1598
1642
  * const trace = MachineTest.run(machine, { events: [] })
@@ -1647,9 +1691,9 @@ export interface StateCoverage<Path extends string = string> {
1647
1691
  * One stable transition-definition identity in definition order.
1648
1692
  *
1649
1693
  * @category models
1650
- * @since 0.4.0
1694
+ * @since 0.14.0
1651
1695
  */
1652
- export interface TransitionCoverageItem<
1696
+ export interface TransitionDefinitionCoverageItem<
1653
1697
  SourcePath extends string = string,
1654
1698
  EventTag extends PropertyKey = PropertyKey,
1655
1699
  TargetPath extends string = SourcePath
@@ -1659,7 +1703,43 @@ export interface TransitionCoverageItem<
1659
1703
  readonly source: SourcePath
1660
1704
  readonly trigger: Machine.Machine.TransitionTrigger<EventTag>
1661
1705
  readonly reenter: boolean
1662
- readonly targets: Machine.Machine.TransitionTargets<TargetPath>
1706
+ readonly branches: ReadonlyArray<Machine.Machine.TransitionBranch<TargetPath>>
1707
+ }
1708
+
1709
+ /**
1710
+ * One statically declared transition branch together with its owning
1711
+ * definition identity.
1712
+ *
1713
+ * @category models
1714
+ * @since 0.14.0
1715
+ */
1716
+ export interface TransitionBranchCoverageItem<
1717
+ SourcePath extends string = string,
1718
+ EventTag extends PropertyKey = PropertyKey,
1719
+ TargetPath extends string = SourcePath
1720
+ > {
1721
+ readonly id: string
1722
+ readonly definitionIndex: number
1723
+ readonly branchIndex: number
1724
+ readonly source: SourcePath
1725
+ readonly trigger: Machine.Machine.TransitionTrigger<EventTag>
1726
+ readonly reenter: boolean
1727
+ readonly branch: Machine.Machine.TransitionBranch<TargetPath>
1728
+ }
1729
+
1730
+ /**
1731
+ * Definition-level and exact branch-level transition coverage.
1732
+ *
1733
+ * @category models
1734
+ * @since 0.14.0
1735
+ */
1736
+ export interface TransitionCoverage<
1737
+ SourcePath extends string = string,
1738
+ EventTag extends PropertyKey = PropertyKey,
1739
+ TargetPath extends string = SourcePath
1740
+ > {
1741
+ readonly definitions: CoverageSummary<TransitionDefinitionCoverageItem<SourcePath, EventTag, TargetPath>>
1742
+ readonly branches: CoverageSummary<TransitionBranchCoverageItem<SourcePath, EventTag, TargetPath>>
1663
1743
  }
1664
1744
 
1665
1745
  /**
@@ -1782,12 +1862,10 @@ export interface HistoryCoverageEvidence<Path extends string = string> {
1782
1862
  */
1783
1863
  export interface Coverage<M extends AnyMachine> {
1784
1864
  readonly states: StateCoverage<StatePath<M>>
1785
- readonly transitions: CoverageSummary<
1786
- TransitionCoverageItem<
1787
- StateNodePath<M>,
1788
- Machine.Machine.TagOf<Machine.Machine.Events<M>[number]>,
1789
- StateNodePath<M>
1790
- >
1865
+ readonly transitions: TransitionCoverage<
1866
+ StateNodePath<M>,
1867
+ Machine.Machine.TagOf<Machine.Machine.Events<M>[number]>,
1868
+ StateNodePath<M>
1791
1869
  >
1792
1870
  readonly events: EventCoverage<Machine.Machine.TagOf<Machine.Machine.InputEvents<M>[number]>>
1793
1871
  readonly scenarios: ScenarioCoverage
@@ -1802,10 +1880,10 @@ export interface Coverage<M extends AnyMachine> {
1802
1880
  }
1803
1881
 
1804
1882
  /**
1805
- * Computes deterministic, definition-aware coverage from completed planner
1806
- * traces. Finite declared sets report hits and misses; scenarios and logical
1807
- * configurations report observations only because their complete spaces are
1808
- * generally infinite.
1883
+ * Computes deterministic, definition- and branch-aware coverage from
1884
+ * completed planner traces. Finite declared sets report hits and misses;
1885
+ * scenarios and logical configurations report observations only because their
1886
+ * complete spaces are generally infinite.
1809
1887
  *
1810
1888
  * **Example**
1811
1889
  *
@@ -1819,7 +1897,10 @@ export interface Coverage<M extends AnyMachine> {
1819
1897
  * const machine = Machine.make({
1820
1898
  * states: States.states,
1821
1899
  * events: Machine.events(),
1822
- * initial: () => States.initial.Idle.from()
1900
+ * initial: {
1901
+ * target: (to) => to.Idle(),
1902
+ * resolve: ({ target }) => target.from()
1903
+ * }
1823
1904
  * }).handle({ Idle: {} })
1824
1905
  *
1825
1906
  * const report = Effect.map(
@@ -1946,7 +2027,7 @@ export type VerificationLawGroup =
1946
2027
  | "microsteps"
1947
2028
  | "completion"
1948
2029
  | "history"
1949
- | "targetBounds"
2030
+ | "definitions"
1950
2031
 
1951
2032
  /**
1952
2033
  * Stable identifiers for individual planner laws.
@@ -1979,8 +2060,11 @@ export type VerificationLaw =
1979
2060
  | "history.value"
1980
2061
  | "history.shallow"
1981
2062
  | "history.deep"
1982
- | "targetBounds.definition"
1983
- | "targetBounds.target"
2063
+ | "definitions.initial"
2064
+ | "definitions.transition"
2065
+ | "definitions.branchIndex"
2066
+ | "definitions.selection"
2067
+ | "definitions.resolution"
1984
2068
 
1985
2069
  /**
1986
2070
  * One independently observed violation in a planner trace.
@@ -2017,7 +2101,10 @@ export interface VerifyOptions {
2017
2101
 
2018
2102
  /**
2019
2103
  * Verifies an executed trace using only public machine inspection and raw
2020
- * snapshot data. The verifier deliberately does not reuse planner
2104
+ * snapshot data. Retained transitions are checked against their exact static
2105
+ * branch, target selection, and resolved route. Startup is checked against
2106
+ * the declared initial root or an exact retained initial-choice route. The
2107
+ * verifier deliberately does not reuse planner
2021
2108
  * normalization, encoding, finality, or other internal helpers.
2022
2109
  *
2023
2110
  * Every selected law is evaluated and returned in one structured error so a
@@ -2035,7 +2122,10 @@ export interface VerifyOptions {
2035
2122
  * const machine = Machine.make({
2036
2123
  * states: States.states,
2037
2124
  * events: Machine.events(),
2038
- * initial: () => States.initial.Idle.from()
2125
+ * initial: {
2126
+ * target: (to) => to.Idle(),
2127
+ * resolve: ({ target }) => target.from()
2128
+ * }
2039
2129
  * }).handle({ Idle: {} })
2040
2130
  *
2041
2131
  * const checked = Effect.gen(function*() {
@@ -316,7 +316,10 @@ export const layerMemory: Layer.Layer<Storage> = internal.layerMemory
316
316
  * const machine = Machine.make({
317
317
  * states: States.states,
318
318
  * events: Machine.events(),
319
- * initial: () => States.initial.Idle.from()
319
+ * initial: {
320
+ * target: (to) => to.Idle(),
321
+ * resolve: ({ target }) => target.from()
322
+ * }
320
323
  * }).handle({ Idle: {} })
321
324
  *
322
325
  * const adapter = ClusterMachine.make("IdleMachine", machine, { version: "1" })
@@ -370,7 +370,10 @@ type ChildState<Child extends Machine.ChildMachine.Any> = RefState<Machine.Child
370
370
  * const machine = Machine.make({
371
371
  * states: States.states,
372
372
  * events: Machine.events(),
373
- * initial: () => States.initial.Count(new Count({ value: 0 }))
373
+ * initial: {
374
+ * target: (to) => to.Count(),
375
+ * resolve: ({ target }) => target(new Count({ value: 0 }))
376
+ * }
374
377
  * }).handle({ Count: {} })
375
378
  * const machineAtom = AtomMachine.make(machine)
376
379
  *
@@ -481,7 +484,10 @@ export const selectSnapshotChild: <
481
484
  * const machine = Machine.make({
482
485
  * states: States.states,
483
486
  * events: Machine.events(),
484
- * initial: () => States.initial.Idle.from()
487
+ * initial: {
488
+ * target: (to) => to.Idle(),
489
+ * resolve: ({ target }) => target.from()
490
+ * }
485
491
  * }).handle({ Idle: {} })
486
492
  * const machineAtom = AtomMachine.make(machine)
487
493
  *
@@ -651,7 +657,10 @@ export interface Bound<Services, RuntimeError = never> {
651
657
  * const machine = Machine.make({
652
658
  * states: States.states,
653
659
  * events: Machine.events(),
654
- * initial: () => States.initial.Idle.from()
660
+ * initial: {
661
+ * target: (to) => to.Idle(),
662
+ * resolve: ({ target }) => target.from()
663
+ * }
655
664
  * }).handle({ Idle: {} })
656
665
  *
657
666
  * const machineAtom = AtomMachine.make(machine)