@@ -1538,11 +1538,24 @@ export const verify = <M extends AnyMachine>(
15381538 return direction === "entry" ? leftOrder - rightOrder : rightOrder - leftOrder
15391539 } )
15401540
1541+ const transitionBranch = (
1542+ transition : Microstep < M > [ "transitions" ] [ number ]
1543+ ) : Machine . Machine . TransitionBranch | undefined => {
1544+ const definition = definitions . find ( ( candidate ) =>
1545+ candidate . source === transition . source && candidate . reenter === transition . reenter &&
1546+ sameTransitionTrigger ( candidate . trigger , transition . trigger )
1547+ )
1548+ return definition === undefined || ! Number . isSafeInteger ( transition . branchIndex ) || transition . branchIndex < 0 ||
1549+ transition . branchIndex >= definition . branches . length
1550+ ? undefined
1551+ : definition . branches [ transition . branchIndex ]
1552+ }
1553+
15411554 const validateTransitionDefinition = (
15421555 transition : Microstep < M > [ "transitions" ] [ number ] ,
15431556 location : VerificationLocation
1544- ) : void => {
1545- if ( ! selected . has ( "definitions" ) ) return
1557+ ) : Machine . Machine . TransitionBranch | undefined => {
1558+ if ( ! selected . has ( "definitions" ) ) return undefined
15461559 const definition = definitions . find ( ( candidate ) =>
15471560 candidate . source === transition . source && candidate . reenter === transition . reenter &&
15481561 sameTransitionTrigger ( candidate . trigger , transition . trigger )
@@ -1554,7 +1567,7 @@ export const verify = <M extends AnyMachine>(
15541567 `retained transition from "${ transition . source } " has no public definition` ,
15551568 transition . source
15561569 )
1557- return
1570+ return undefined
15581571 }
15591572 if (
15601573 ! Number . isSafeInteger ( transition . branchIndex ) || transition . branchIndex < 0 ||
@@ -1566,7 +1579,7 @@ export const verify = <M extends AnyMachine>(
15661579 `retained transition from "${ transition . source } " selected invalid branch index ${ transition . branchIndex } ` ,
15671580 transition . source
15681581 )
1569- return
1582+ return undefined
15701583 }
15711584 const branch = definition . branches [ transition . branchIndex ] !
15721585 if ( ! targetWithinSelection ( transition . target , branch , byPath ) ) {
@@ -1581,6 +1594,141 @@ export const verify = <M extends AnyMachine>(
15811594 transition . target === undefined ? transition . source : String ( transition . target )
15821595 )
15831596 }
1597+ return branch
1598+ }
1599+
1600+ const validateTransitionResolutions = (
1601+ transitions : Microstep < M > [ "transitions" ] ,
1602+ branches : ReadonlyArray < Machine . Machine . TransitionBranch | undefined > ,
1603+ location : VerificationLocation
1604+ ) : void => {
1605+ if ( ! selected . has ( "definitions" ) ) return
1606+
1607+ const choiceResolution = (
1608+ start : string ,
1609+ afterIndex : number ,
1610+ nested : boolean
1611+ ) : { readonly found : boolean ; readonly target : string | undefined } => {
1612+ const seen = new Set < string > ( )
1613+ let choice = start
1614+ let cursor = afterIndex + 1
1615+ let first = true
1616+ while ( ! seen . has ( choice ) ) {
1617+ seen . add ( choice )
1618+ let choiceIndex = - 1
1619+ for ( let index = cursor ; index < transitions . length ; index ++ ) {
1620+ const candidate = transitions [ index ] !
1621+ if (
1622+ candidate . trigger . type === "choice" &&
1623+ ( candidate . source === choice || first && nested && isDescendantOrSelf ( String ( candidate . source ) , choice ) )
1624+ ) {
1625+ choiceIndex = index
1626+ break
1627+ }
1628+ }
1629+ if ( choiceIndex === - 1 ) return { found : false , target : undefined }
1630+ const transition = transitions [ choiceIndex ] !
1631+ if ( branches [ choiceIndex ] === undefined ) return { found : false , target : undefined }
1632+ const target = transition . target === undefined ? undefined : String ( transition . target )
1633+ if ( target === undefined ) return { found : true , target : undefined }
1634+ const targetNode = byPath . get ( target )
1635+ if ( targetNode ?. type === "choice" ) {
1636+ choice = target
1637+ cursor = choiceIndex + 1
1638+ first = false
1639+ continue
1640+ }
1641+ return {
1642+ found : true ,
1643+ target : targetNode ?. type === "history" ? targetNode . parent : target
1644+ }
1645+ }
1646+ return { found : false , target : undefined }
1647+ }
1648+
1649+ transitions . forEach ( ( transition , index ) => {
1650+ if ( branches [ index ] === undefined ) return
1651+ const target = transition . target === undefined ? undefined : String ( transition . target )
1652+ const resolvedTarget = transition . resolvedTarget === undefined ? undefined : String ( transition . resolvedTarget )
1653+ const targetNode = target === undefined ? undefined : byPath . get ( target )
1654+ let expected = target
1655+ let explanation = target === undefined ? "an unresolved targetless transition" : `target "${ target } "`
1656+
1657+ if ( transition . trigger . type === "choice" ) {
1658+ explanation = target === undefined ? "a targetless choice edge" : `choice edge target "${ target } "`
1659+ } else if ( targetNode ?. type === "history" ) {
1660+ expected = targetNode . parent
1661+ explanation = `history owner "${ String ( targetNode . parent ) } "`
1662+ } else if ( targetNode ?. type === "choice" ) {
1663+ const resolution = choiceResolution ( targetNode . path , index , false )
1664+ if ( ! resolution . found ) {
1665+ add (
1666+ "definitions.resolution" ,
1667+ location ,
1668+ `transition from "${ transition . source } " targets choice "${ target } " without an exact retained route` ,
1669+ target
1670+ )
1671+ return
1672+ }
1673+ expected = resolution . target
1674+ explanation = expected === undefined ?
1675+ `targetless route from choice "${ target } "` :
1676+ `choice route from "${ target } " to "${ expected } "`
1677+ } else if (
1678+ resolvedTarget !== target &&
1679+ ( targetNode ?. type === "compound" || targetNode ?. type === "parallel" )
1680+ ) {
1681+ const resolution = choiceResolution ( targetNode . path , index , true )
1682+ if ( resolution . found ) {
1683+ expected = resolution . target
1684+ explanation = expected === undefined ?
1685+ `targetless nested choice route from "${ target } "` :
1686+ `nested choice route from "${ target } " to "${ expected } "`
1687+ }
1688+ }
1689+
1690+ if ( resolvedTarget !== expected ) {
1691+ add (
1692+ "definitions.resolution" ,
1693+ location ,
1694+ `transition branch ${ transition . branchIndex } from "${ transition . source } " resolved to ` +
1695+ `"${ String ( transition . resolvedTarget ) } " instead of ${ explanation } ` ,
1696+ resolvedTarget ?? target ?? String ( transition . source )
1697+ )
1698+ }
1699+ } )
1700+ }
1701+
1702+ const initialChoiceRouteRoot = ( ) : string | undefined => {
1703+ const routing = trace . initial . plan . microsteps [ 0 ]
1704+ if (
1705+ routing === undefined || routing . changed ||
1706+ routing . transitions . length === 0 ||
1707+ ! routing . transitions . every ( ( transition ) => transition . trigger . type === "choice" )
1708+ ) {
1709+ return undefined
1710+ }
1711+
1712+ const reachableScopes : Array < string > = [ initialDefinition . target ]
1713+ let terminalRoot : string | undefined
1714+ for ( const transition of routing . transitions ) {
1715+ const source = String ( transition . source )
1716+ const branch = transitionBranch ( transition )
1717+ if (
1718+ branch === undefined || ! targetWithinSelection ( transition . target , branch , byPath ) ||
1719+ ! reachableScopes . some ( ( scope ) => isDescendantOrSelf ( source , scope ) )
1720+ ) {
1721+ return undefined
1722+ }
1723+ const target = transition . target === undefined ? undefined : String ( transition . target )
1724+ if ( target === undefined ) return undefined
1725+ const targetNode = byPath . get ( target )
1726+ const scope = targetNode ?. type === "history" ? targetNode . parent : target
1727+ if ( scope === undefined ) return undefined
1728+ reachableScopes . push ( scope )
1729+ terminalRoot = ancestors ( scope ) [ 0 ]
1730+ }
1731+ return terminalRoot
15841732 }
15851733
15861734 const validateMicrostep = (
@@ -1735,7 +1883,8 @@ export const verify = <M extends AnyMachine>(
17351883 }
17361884 }
17371885 }
1738- for ( const transition of microstep . transitions ) validateTransitionDefinition ( transition , location )
1886+ const branches = microstep . transitions . map ( ( transition ) => validateTransitionDefinition ( transition , location ) )
1887+ validateTransitionResolutions ( microstep . transitions , branches , location )
17391888 }
17401889
17411890 const validatePlanCompletion = (
@@ -1773,11 +1922,17 @@ export const verify = <M extends AnyMachine>(
17731922 const starting = inspectSnapshot ( trace . initial . startingState , initialLocation , "initial starting state" )
17741923 if ( selected . has ( "definitions" ) ) {
17751924 const startingRoots = starting . paths . filter ( ( path ) => byPath . get ( path ) ?. parent === undefined )
1776- if ( startingRoots . length !== 1 || startingRoots [ 0 ] !== initialDefinition . target ) {
1925+ const routedRoot = startingRoots . length === 1 && startingRoots [ 0 ] !== initialDefinition . target
1926+ ? initialChoiceRouteRoot ( )
1927+ : undefined
1928+ if (
1929+ startingRoots . length !== 1 ||
1930+ startingRoots [ 0 ] !== initialDefinition . target && startingRoots [ 0 ] !== routedRoot
1931+ ) {
17771932 add (
17781933 "definitions.initial" ,
17791934 initialLocation ,
1780- `initial starting state selected roots [${ startingRoots . join ( ", " ) } ] instead of ` +
1935+ `initial starting state selected roots [${ startingRoots . join ( ", " ) } ] without an exact route from ` +
17811936 `declared root "${ initialDefinition . target } "` ,
17821937 startingRoots [ 0 ] ?? initialDefinition . target
17831938 )
0 commit comments