@typeonce/effect-machine 0.12.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.
- package/README.md +130 -30
- package/dist/Machine.d.ts +722 -262
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +130 -62
- package/dist/Machine.js.map +1 -1
- package/dist/internal/machine/atom.d.ts +1 -0
- package/dist/internal/machine/atom.d.ts.map +1 -1
- package/dist/internal/machine/atom.js +23 -0
- package/dist/internal/machine/atom.js.map +1 -1
- package/dist/internal/machine/commandRuntime.d.ts.map +1 -1
- package/dist/internal/machine/commandRuntime.js +2 -5
- package/dist/internal/machine/commandRuntime.js.map +1 -1
- package/dist/internal/machine/configuration.d.ts +5 -0
- package/dist/internal/machine/configuration.d.ts.map +1 -1
- package/dist/internal/machine/configuration.js +6 -2
- package/dist/internal/machine/configuration.js.map +1 -1
- package/dist/internal/machine/executionPlan.d.ts +1 -0
- package/dist/internal/machine/executionPlan.d.ts.map +1 -1
- package/dist/internal/machine/executionPlan.js +57 -24
- package/dist/internal/machine/executionPlan.js.map +1 -1
- package/dist/internal/machine/initialization.d.ts +5 -0
- package/dist/internal/machine/initialization.d.ts.map +1 -0
- package/dist/internal/machine/initialization.js +61 -0
- package/dist/internal/machine/initialization.js.map +1 -0
- package/dist/internal/machine/inspectionRuntime.d.ts +25 -0
- package/dist/internal/machine/inspectionRuntime.d.ts.map +1 -0
- package/dist/internal/machine/inspectionRuntime.js +79 -0
- package/dist/internal/machine/inspectionRuntime.js.map +1 -0
- package/dist/internal/machine/invocation.d.ts +2 -1
- package/dist/internal/machine/invocation.d.ts.map +1 -1
- package/dist/internal/machine/invocation.js +17 -10
- package/dist/internal/machine/invocation.js.map +1 -1
- package/dist/internal/machine/machine.d.ts +4 -3
- package/dist/internal/machine/machine.d.ts.map +1 -1
- package/dist/internal/machine/machine.js +327 -32
- package/dist/internal/machine/machine.js.map +1 -1
- package/dist/internal/machine/planner.d.ts +28 -0
- package/dist/internal/machine/planner.d.ts.map +1 -1
- package/dist/internal/machine/planner.js +132 -51
- package/dist/internal/machine/planner.js.map +1 -1
- package/dist/internal/machine/process.d.ts.map +1 -1
- package/dist/internal/machine/process.js +4 -2
- package/dist/internal/machine/process.js.map +1 -1
- package/dist/internal/machine/runtime.d.ts +27 -3
- package/dist/internal/machine/runtime.d.ts.map +1 -1
- package/dist/internal/machine/runtime.js +440 -64
- package/dist/internal/machine/runtime.js.map +1 -1
- package/dist/internal/machine/symbols.d.ts +2 -0
- package/dist/internal/machine/symbols.d.ts.map +1 -1
- package/dist/internal/machine/symbols.js +2 -0
- package/dist/internal/machine/symbols.js.map +1 -1
- package/dist/internal/machine/topology.d.ts +25 -0
- package/dist/internal/machine/topology.d.ts.map +1 -1
- package/dist/internal/machine/topology.js +26 -10
- package/dist/internal/machine/topology.js.map +1 -1
- package/dist/internal/testing/machine/exploration.d.ts.map +1 -1
- package/dist/internal/testing/machine/exploration.js +11 -2
- package/dist/internal/testing/machine/exploration.js.map +1 -1
- package/dist/internal/testing/machine/finiteModel.d.ts.map +1 -1
- package/dist/internal/testing/machine/finiteModel.js +75 -69
- package/dist/internal/testing/machine/finiteModel.js.map +1 -1
- package/dist/internal/testing/machine/transitionCoverage.d.ts +20 -0
- package/dist/internal/testing/machine/transitionCoverage.d.ts.map +1 -0
- package/dist/internal/testing/machine/transitionCoverage.js +80 -0
- package/dist/internal/testing/machine/transitionCoverage.js.map +1 -0
- package/dist/internal/testing/machine/verification.d.ts.map +1 -1
- package/dist/internal/testing/machine/verification.js +171 -33
- package/dist/internal/testing/machine/verification.js.map +1 -1
- package/dist/testing/MachineTest.d.ts +97 -22
- package/dist/testing/MachineTest.d.ts.map +1 -1
- package/dist/testing/MachineTest.js +58 -16
- package/dist/testing/MachineTest.js.map +1 -1
- package/dist/unstable/cluster/ClusterMachine.d.ts +4 -1
- package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -1
- package/dist/unstable/cluster/ClusterMachine.js +4 -1
- package/dist/unstable/cluster/ClusterMachine.js.map +1 -1
- package/dist/unstable/reactivity/AtomMachine.d.ts +21 -3
- package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
- package/dist/unstable/reactivity/AtomMachine.js +21 -3
- package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
- package/docs/agent-guide.md +240 -65
- package/package.json +5 -5
- package/src/Machine.ts +1240 -495
- package/src/internal/machine/atom.ts +26 -0
- package/src/internal/machine/commandRuntime.ts +2 -9
- package/src/internal/machine/configuration.ts +28 -3
- package/src/internal/machine/executionPlan.ts +72 -24
- package/src/internal/machine/initialization.ts +73 -0
- package/src/internal/machine/inspectionRuntime.ts +102 -0
- package/src/internal/machine/invocation.ts +37 -12
- package/src/internal/machine/machine.ts +438 -39
- package/src/internal/machine/planner.ts +180 -55
- package/src/internal/machine/process.ts +4 -2
- package/src/internal/machine/runtime.ts +661 -89
- package/src/internal/machine/symbols.ts +3 -0
- package/src/internal/machine/topology.ts +65 -12
- package/src/internal/testing/machine/exploration.ts +10 -2
- package/src/internal/testing/machine/finiteModel.ts +114 -80
- package/src/internal/testing/machine/transitionCoverage.ts +116 -0
- package/src/internal/testing/machine/verification.ts +216 -58
- package/src/testing/MachineTest.ts +118 -28
- package/src/unstable/cluster/ClusterMachine.ts +4 -1
- package/src/unstable/reactivity/AtomMachine.ts +24 -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
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
|
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:
|
|
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", "
|
|
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
|
|
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
|
-
):
|
|
1500
|
-
if (!selected.has("
|
|
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
|
-
|
|
1491
|
+
sameTransitionTrigger(candidate.trigger, transition.trigger)
|
|
1504
1492
|
)
|
|
1505
1493
|
if (definition === undefined) {
|
|
1506
1494
|
add(
|
|
1507
|
-
"
|
|
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
|
-
|
|
1515
|
-
if (!
|
|
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
|
-
"
|
|
1520
|
+
"definitions.selection",
|
|
1518
1521
|
location,
|
|
1519
|
-
`transition
|
|
1520
|
-
`
|
|
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
|
-
|
|
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:
|
|
235
|
-
*
|
|
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:
|
|
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:
|
|
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:
|
|
1443
|
+
* initial: {
|
|
1444
|
+
* target: (to) => to.Count(),
|
|
1445
|
+
* resolve: ({ target }) => target(new Count({ value: 0 }))
|
|
1446
|
+
* }
|
|
1412
1447
|
* }).handle({
|
|
1413
|
-
* Count: {
|
|
1414
|
-
*
|
|
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:
|
|
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.
|
|
1694
|
+
* @since 0.14.0
|
|
1651
1695
|
*/
|
|
1652
|
-
export interface
|
|
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
|
|
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:
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
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
|
|
1806
|
-
* traces. Finite declared sets report hits and misses;
|
|
1807
|
-
* configurations report observations only because their
|
|
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:
|
|
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
|
-
| "
|
|
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
|
-
| "
|
|
1983
|
-
| "
|
|
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.
|
|
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:
|
|
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:
|
|
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" })
|