@@ -29,7 +29,8 @@ import type {
2929 StateCoverageItem ,
3030 Trace ,
3131 TraceStep ,
32- TransitionCoverageItem ,
32+ TransitionBranchCoverageItem ,
33+ TransitionDefinitionCoverageItem ,
3334 VerificationLaw ,
3435 VerificationLawGroup ,
3536 VerificationViolation ,
@@ -469,21 +470,33 @@ const normalizeTraces = <M extends AnyMachine>(
469470) : ReadonlyArray < Trace < M > > =>
470471 Array . isArray ( traceOrTraces ) ? traceOrTraces as ReadonlyArray < Trace < M > > : [ traceOrTraces as Trace < M > ]
471472
472- const sameCoverageTrigger = (
473+ const sameTransitionTrigger = (
473474 left : Machine . Machine . TransitionTrigger ,
474475 right : Machine . Machine . TransitionTrigger
475- ) : boolean =>
476- left . type === right . type && ( left . type !== "event" || right . type === "event" && left . event === right . event )
476+ ) : boolean => {
477+ if ( left . type !== right . type ) return false
478+ if ( left . type === "event" ) return right . type === "event" && left . event === right . event
479+ if ( left . type === "invoke" ) {
480+ return right . type === "invoke" && left . id === right . id && left . outcome === right . outcome
481+ }
482+ return true
483+ }
477484
478- const targetWithinDeclaredBranches = (
485+ const targetWithinSelection = (
479486 target : string | undefined ,
480- branches : ReadonlyArray < Machine . Machine . TransitionBranch >
481- ) : boolean =>
482- branches . some ( ( branch ) =>
483- branch . target === undefined ?
484- target === undefined
485- : target !== undefined && ( target === branch . target || target . startsWith ( `${ branch . target } .` ) )
486- )
487+ branch : Machine . Machine . TransitionBranch ,
488+ nodeByPath : ReadonlyMap < string , Machine . Machine . StateNode >
489+ ) : boolean => {
490+ const selection = branch . selection
491+ if ( selection . kind === "none" ) return target === undefined
492+ if ( target === undefined || selection . path === undefined ) return false
493+ if ( target === selection . path ) return true
494+ const selectedNode = nodeByPath . get ( selection . path )
495+ return selection . kind === "state" &&
496+ ( selection . scope === "local" || selection . scope === "branch" ) &&
497+ ( selectedNode ?. type === "compound" || selectedNode ?. type === "parallel" ) &&
498+ target . startsWith ( `${ selection . path } .` )
499+ }
487500
488501const finiteTagValues = ( ast : SchemaAST . AST ) : ReadonlyArray < PropertyKey > | undefined => {
489502 if ( SchemaAST . isLiteral ( ast ) ) {
@@ -567,12 +580,12 @@ export const coverage = <M extends AnyMachine>(
567580 (
568581 definition ,
569582 index
570- ) : TransitionCoverageItem <
583+ ) : TransitionDefinitionCoverageItem <
571584 StateNodePath < M > ,
572585 Machine . Machine . TagOf < Machine . Machine . Events < M > [ number ] > ,
573586 StateNodePath < M >
574587 > => ( {
575- id : `transition:${ index } : ${ formatValue ( definition ) } ` ,
588+ id : `transition:${ index } ` ,
576589 index,
577590 source : definition . source ,
578591 trigger : definition . trigger ,
@@ -581,6 +594,30 @@ export const coverage = <M extends AnyMachine>(
581594 } )
582595 )
583596 const transitionHits = new Set < number > ( )
597+ const branchOffsets : Array < number > = [ ]
598+ const branches : Array <
599+ TransitionBranchCoverageItem <
600+ StateNodePath < M > ,
601+ Machine . Machine . TagOf < Machine . Machine . Events < M > [ number ] > ,
602+ StateNodePath < M >
603+ >
604+ > = [ ]
605+ for ( let definitionIndex = 0 ; definitionIndex < definitions . length ; definitionIndex ++ ) {
606+ branchOffsets . push ( branches . length )
607+ const definition = definitions [ definitionIndex ] !
608+ definition . branches . forEach ( ( branch , branchIndex ) => {
609+ branches . push ( {
610+ id : `transition:${ definitionIndex } :branch:${ branchIndex } ` ,
611+ definitionIndex,
612+ branchIndex,
613+ source : definition . source ,
614+ trigger : definition . trigger ,
615+ reenter : definition . reenter ,
616+ branch
617+ } )
618+ } )
619+ }
620+ const branchHits = new Set < number > ( )
584621
585622 const declaredEvents = publicEventTags ( machine )
586623 const declaredEventTags = declaredEvents . tags
@@ -658,10 +695,17 @@ export const coverage = <M extends AnyMachine>(
658695 const definitionIndex = definitions . findIndex ( ( definition ) =>
659696 definition . source === retained . source &&
660697 definition . reenter === retained . reenter &&
661- sameCoverageTrigger ( definition . trigger , retained . trigger ) &&
662- targetWithinDeclaredBranches ( retained . target , definition . branches )
698+ sameTransitionTrigger ( definition . trigger , retained . trigger )
663699 )
664- if ( definitionIndex !== - 1 ) transitionHits . add ( definitionIndex )
700+ if ( definitionIndex === - 1 ) continue
701+ transitionHits . add ( definitionIndex )
702+ const definition = definitions [ definitionIndex ] !
703+ if (
704+ Number . isSafeInteger ( retained . branchIndex ) && retained . branchIndex >= 0 &&
705+ retained . branchIndex < definition . branches . length
706+ ) {
707+ branchHits . add ( branchOffsets [ definitionIndex ] ! + retained . branchIndex )
708+ }
665709 }
666710 }
667711
@@ -701,7 +745,10 @@ export const coverage = <M extends AnyMachine>(
701745 entry : coverageSummary ( activeNodes , entryHits ) ,
702746 exit : coverageSummary ( activeNodes , exitHits )
703747 } ,
704- transitions : coverageSummary ( definitions , transitionHits ) ,
748+ transitions : {
749+ definitions : coverageSummary ( definitions , transitionHits ) ,
750+ branches : coverageSummary ( branches , branchHits )
751+ } ,
705752 events : declaredEvents . diagnostics . length === 0
706753 ? {
707754 available : true ,
@@ -969,12 +1016,6 @@ const sameValue = (left: unknown, right: unknown): boolean => formatValue(left)
9691016const samePaths = ( left : ReadonlyArray < string > , right : ReadonlyArray < string > ) : boolean =>
9701017 left . length === right . length && left . every ( ( path , index ) => path === right [ index ] )
9711018
972- const sameTrigger = (
973- left : Machine . Machine . TransitionTrigger ,
974- right : Machine . Machine . TransitionTrigger
975- ) : boolean =>
976- left . type === right . type && ( left . type !== "event" || right . type === "event" && left . event === right . event )
977-
9781019const makeNodeUtilities = ( nodes : ReadonlyArray < PublicStateNode > ) => {
9791020 const byPath = new Map ( nodes . map ( ( node ) => [ node . path , node ] ) )
9801021 const depth = ( path : string ) : number => {
@@ -1018,9 +1059,10 @@ export const verify = <M extends AnyMachine>(
10181059 options : VerifyOptions = { }
10191060) : Effect . Effect < void , VerificationError > => {
10201061 const selected = new Set < VerificationLawGroup > (
1021- options . laws ?? [ "configuration" , "microsteps" , "completion" , "history" , "targetBounds " ]
1062+ options . laws ?? [ "configuration" , "microsteps" , "completion" , "history" , "definitions " ]
10221063 )
10231064 const nodes = Machine . stateNodes ( machine ) as ReadonlyArray < PublicStateNode >
1065+ const initialDefinition = Machine . initialDefinition ( machine )
10241066 const definitions = Machine . transitionDefinitions ( machine )
10251067 const { ancestors, byPath, depth, isDescendantOrSelf } = makeNodeUtilities ( nodes )
10261068 const violations : Array < VerificationViolation > = [ ]
@@ -1496,33 +1538,47 @@ export const verify = <M extends AnyMachine>(
14961538 return direction === "entry" ? leftOrder - rightOrder : rightOrder - leftOrder
14971539 } )
14981540
1499- const validateTransitionBounds = (
1541+ const validateTransitionDefinition = (
15001542 transition : Microstep < M > [ "transitions" ] [ number ] ,
15011543 location : VerificationLocation
15021544 ) : void => {
1503- if ( ! selected . has ( "targetBounds " ) ) return
1545+ if ( ! selected . has ( "definitions " ) ) return
15041546 const definition = definitions . find ( ( candidate ) =>
15051547 candidate . source === transition . source && candidate . reenter === transition . reenter &&
1506- sameTrigger ( candidate . trigger , transition . trigger )
1548+ sameTransitionTrigger ( candidate . trigger , transition . trigger )
15071549 )
15081550 if ( definition === undefined ) {
15091551 add (
1510- "targetBounds.definition " ,
1552+ "definitions.transition " ,
15111553 location ,
15121554 `retained transition from "${ transition . source } " has no public definition` ,
15131555 transition . source
15141556 )
15151557 return
15161558 }
1517- if ( targetWithinDeclaredBranches ( transition . target , definition . branches ) ) return
1518- const targets = definition . branches . flatMap ( ( branch ) => branch . target === undefined ? [ ] : [ branch . target ] )
1519- if ( ! targets . some ( ( bound ) => isDescendantOrSelf ( String ( transition . target ) , String ( bound ) ) ) ) {
1559+ if (
1560+ ! Number . isSafeInteger ( transition . branchIndex ) || transition . branchIndex < 0 ||
1561+ transition . branchIndex >= definition . branches . length
1562+ ) {
1563+ add (
1564+ "definitions.branchIndex" ,
1565+ location ,
1566+ `retained transition from "${ transition . source } " selected invalid branch index ${ transition . branchIndex } ` ,
1567+ transition . source
1568+ )
1569+ return
1570+ }
1571+ const branch = definition . branches [ transition . branchIndex ] !
1572+ if ( ! targetWithinSelection ( transition . target , branch , byPath ) ) {
1573+ const expected = branch . selection . kind === "none"
1574+ ? "an explicitly targetless result"
1575+ : `selection ${ branch . selection . kind } :${ branch . selection . scope } :${ String ( branch . selection . path ) } `
15201576 add (
1521- "targetBounds.target " ,
1577+ "definitions.selection " ,
15221578 location ,
1523- `transition target "${ String ( transition . target ) } " is outside declared bounds ` +
1524- `[ ${ targets . join ( ", " ) } ] ` ,
1525- String ( transition . target )
1579+ `transition branch ${ transition . branchIndex } from "${ transition . source } " returned ` +
1580+ `target " ${ String ( transition . target ) } " outside ${ expected } ` ,
1581+ transition . target === undefined ? transition . source : String ( transition . target )
15261582 )
15271583 }
15281584 }
@@ -1679,7 +1735,7 @@ export const verify = <M extends AnyMachine>(
16791735 }
16801736 }
16811737 }
1682- for ( const transition of microstep . transitions ) validateTransitionBounds ( transition , location )
1738+ for ( const transition of microstep . transitions ) validateTransitionDefinition ( transition , location )
16831739 }
16841740
16851741 const validatePlanCompletion = (
@@ -1715,6 +1771,18 @@ export const verify = <M extends AnyMachine>(
17151771
17161772 const initialLocation : VerificationLocation = { eventIndex : undefined }
17171773 const starting = inspectSnapshot ( trace . initial . startingState , initialLocation , "initial starting state" )
1774+ if ( selected . has ( "definitions" ) ) {
1775+ const startingRoots = starting . paths . filter ( ( path ) => byPath . get ( path ) ?. parent === undefined )
1776+ if ( startingRoots . length !== 1 || startingRoots [ 0 ] !== initialDefinition . target ) {
1777+ add (
1778+ "definitions.initial" ,
1779+ initialLocation ,
1780+ `initial starting state selected roots [${ startingRoots . join ( ", " ) } ] instead of ` +
1781+ `declared root "${ initialDefinition . target } "` ,
1782+ startingRoots [ 0 ] ?? initialDefinition . target
1783+ )
1784+ }
1785+ }
17181786 validateSnapshotMetadata ( starting , initialLocation , "initial starting state" )
17191787 validateTraceConfiguration (
17201788 starting ,
@@ -1849,6 +1917,7 @@ const formatMicrosteps = <M extends AnyMachine>(microsteps: ReadonlyArray<Micros
18491917 source : transition . source ,
18501918 trigger : transition . trigger ,
18511919 reenter : transition . reenter ,
1920+ branchIndex : transition . branchIndex ,
18521921 target : transition . target ,
18531922 resolvedTarget : transition . resolvedTarget
18541923 } ) )
0 commit comments