@typeonce/effect-machine 0.31.2 → 0.32.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 (151) hide show
  1. package/README.md +145 -140
  2. package/dist/Machine.d.ts +428 -254
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +91 -111
  5. package/dist/Machine.js.map +1 -1
  6. package/dist/internal/machine/atom.d.ts +3 -4
  7. package/dist/internal/machine/atom.d.ts.map +1 -1
  8. package/dist/internal/machine/atom.js +0 -2
  9. package/dist/internal/machine/atom.js.map +1 -1
  10. package/dist/internal/machine/childRegistry.d.ts +52 -0
  11. package/dist/internal/machine/childRegistry.d.ts.map +1 -0
  12. package/dist/internal/machine/childRegistry.js +74 -0
  13. package/dist/internal/machine/childRegistry.js.map +1 -0
  14. package/dist/internal/machine/cluster.d.ts +2 -3
  15. package/dist/internal/machine/cluster.d.ts.map +1 -1
  16. package/dist/internal/machine/cluster.js.map +1 -1
  17. package/dist/internal/machine/configuration.d.ts.map +1 -1
  18. package/dist/internal/machine/configuration.js +14 -4
  19. package/dist/internal/machine/configuration.js.map +1 -1
  20. package/dist/internal/machine/executionPlan.d.ts +1 -0
  21. package/dist/internal/machine/executionPlan.d.ts.map +1 -1
  22. package/dist/internal/machine/executionPlan.js +97 -53
  23. package/dist/internal/machine/executionPlan.js.map +1 -1
  24. package/dist/internal/machine/invocation.d.ts.map +1 -1
  25. package/dist/internal/machine/invocation.js +4 -2
  26. package/dist/internal/machine/invocation.js.map +1 -1
  27. package/dist/internal/machine/invocationDefinition.d.ts +35 -0
  28. package/dist/internal/machine/invocationDefinition.d.ts.map +1 -0
  29. package/dist/internal/machine/invocationDefinition.js +21 -0
  30. package/dist/internal/machine/invocationDefinition.js.map +1 -0
  31. package/dist/internal/machine/invocationEvent.d.ts +0 -2
  32. package/dist/internal/machine/invocationEvent.d.ts.map +1 -1
  33. package/dist/internal/machine/invocationEvent.js +0 -8
  34. package/dist/internal/machine/invocationEvent.js.map +1 -1
  35. package/dist/internal/machine/machine.d.ts +21 -43
  36. package/dist/internal/machine/machine.d.ts.map +1 -1
  37. package/dist/internal/machine/machine.js +98 -374
  38. package/dist/internal/machine/machine.js.map +1 -1
  39. package/dist/internal/machine/planner.d.ts +13 -19
  40. package/dist/internal/machine/planner.d.ts.map +1 -1
  41. package/dist/internal/machine/planner.js +63 -35
  42. package/dist/internal/machine/planner.js.map +1 -1
  43. package/dist/internal/machine/process.d.ts +6 -8
  44. package/dist/internal/machine/process.d.ts.map +1 -1
  45. package/dist/internal/machine/process.js +1 -0
  46. package/dist/internal/machine/process.js.map +1 -1
  47. package/dist/internal/machine/protocol.d.ts +2 -2
  48. package/dist/internal/machine/protocol.d.ts.map +1 -1
  49. package/dist/internal/machine/protocol.js +4 -18
  50. package/dist/internal/machine/protocol.js.map +1 -1
  51. package/dist/internal/machine/readiness.d.ts +1 -1
  52. package/dist/internal/machine/readiness.d.ts.map +1 -1
  53. package/dist/internal/machine/requirements.d.ts +4 -0
  54. package/dist/internal/machine/requirements.d.ts.map +1 -0
  55. package/dist/internal/machine/requirements.js +2 -0
  56. package/dist/internal/machine/requirements.js.map +1 -0
  57. package/dist/internal/machine/runtime.d.ts +4 -58
  58. package/dist/internal/machine/runtime.d.ts.map +1 -1
  59. package/dist/internal/machine/runtime.js +1 -69
  60. package/dist/internal/machine/runtime.js.map +1 -1
  61. package/dist/internal/machine/serialization.d.ts.map +1 -1
  62. package/dist/internal/machine/serialization.js +16 -0
  63. package/dist/internal/machine/serialization.js.map +1 -1
  64. package/dist/internal/machine/stateDefinition.d.ts +3 -1
  65. package/dist/internal/machine/stateDefinition.d.ts.map +1 -1
  66. package/dist/internal/machine/stateDefinition.js +36 -2
  67. package/dist/internal/machine/stateDefinition.js.map +1 -1
  68. package/dist/internal/machine/targetBuilder.d.ts +16 -0
  69. package/dist/internal/machine/targetBuilder.d.ts.map +1 -0
  70. package/dist/internal/machine/targetBuilder.js +333 -0
  71. package/dist/internal/machine/targetBuilder.js.map +1 -0
  72. package/dist/internal/machine/topology.js +3 -3
  73. package/dist/internal/machine/topology.js.map +1 -1
  74. package/dist/internal/machine/transition.d.ts +16 -0
  75. package/dist/internal/machine/transition.d.ts.map +1 -0
  76. package/dist/internal/machine/transition.js +2 -0
  77. package/dist/internal/machine/transition.js.map +1 -0
  78. package/dist/internal/testing/machine/finiteModel.d.ts.map +1 -1
  79. package/dist/internal/testing/machine/finiteModel.js +10 -12
  80. package/dist/internal/testing/machine/finiteModel.js.map +1 -1
  81. package/dist/internal/testing/machine/format.d.ts +7 -0
  82. package/dist/internal/testing/machine/format.d.ts.map +1 -0
  83. package/dist/internal/testing/machine/format.js +104 -0
  84. package/dist/internal/testing/machine/format.js.map +1 -0
  85. package/dist/internal/testing/machine/probe.d.ts +1 -1
  86. package/dist/internal/testing/machine/probe.d.ts.map +1 -1
  87. package/dist/internal/testing/machine/probe.js +1 -1
  88. package/dist/internal/testing/machine/probe.js.map +1 -1
  89. package/dist/internal/testing/machine/referenceModel.d.ts.map +1 -1
  90. package/dist/internal/testing/machine/referenceModel.js +15 -19
  91. package/dist/internal/testing/machine/referenceModel.js.map +1 -1
  92. package/dist/internal/testing/machine/trace.d.ts +1 -1
  93. package/dist/internal/testing/machine/trace.d.ts.map +1 -1
  94. package/dist/internal/testing/machine/trace.js +3 -3
  95. package/dist/internal/testing/machine/trace.js.map +1 -1
  96. package/dist/internal/testing/machine/value.d.ts +8 -0
  97. package/dist/internal/testing/machine/value.d.ts.map +1 -0
  98. package/dist/internal/testing/machine/value.js +82 -0
  99. package/dist/internal/testing/machine/value.js.map +1 -0
  100. package/dist/internal/testing/machine/verification.d.ts +2 -13
  101. package/dist/internal/testing/machine/verification.d.ts.map +1 -1
  102. package/dist/internal/testing/machine/verification.js +21 -113
  103. package/dist/internal/testing/machine/verification.js.map +1 -1
  104. package/dist/testing/MachineTest.d.ts +89 -104
  105. package/dist/testing/MachineTest.d.ts.map +1 -1
  106. package/dist/testing/MachineTest.js +70 -84
  107. package/dist/testing/MachineTest.js.map +1 -1
  108. package/dist/unstable/cluster/ClusterMachine.d.ts +10 -13
  109. package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -1
  110. package/dist/unstable/cluster/ClusterMachine.js +5 -8
  111. package/dist/unstable/cluster/ClusterMachine.js.map +1 -1
  112. package/dist/unstable/reactivity/AtomMachine.d.ts +26 -61
  113. package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
  114. package/dist/unstable/reactivity/AtomMachine.js +17 -26
  115. package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
  116. package/docs/agent-guide.md +45 -45
  117. package/docs/effect-atom-react.md +35 -63
  118. package/docs/machine-review.md +7 -7
  119. package/docs/root-api.md +214 -0
  120. package/package.json +1 -1
  121. package/src/Machine.ts +778 -437
  122. package/src/internal/machine/atom.ts +3 -13
  123. package/src/internal/machine/childRegistry.ts +141 -0
  124. package/src/internal/machine/cluster.ts +5 -14
  125. package/src/internal/machine/configuration.ts +20 -4
  126. package/src/internal/machine/executionPlan.ts +109 -52
  127. package/src/internal/machine/invocation.ts +10 -6
  128. package/src/internal/machine/invocationDefinition.ts +52 -0
  129. package/src/internal/machine/invocationEvent.ts +0 -8
  130. package/src/internal/machine/machine.ts +154 -679
  131. package/src/internal/machine/planner.ts +74 -62
  132. package/src/internal/machine/process.ts +14 -21
  133. package/src/internal/machine/protocol.ts +9 -32
  134. package/src/internal/machine/readiness.ts +1 -0
  135. package/src/internal/machine/requirements.ts +11 -0
  136. package/src/internal/machine/runtime.ts +17 -186
  137. package/src/internal/machine/serialization.ts +22 -0
  138. package/src/internal/machine/stateDefinition.ts +36 -3
  139. package/src/internal/machine/targetBuilder.ts +525 -0
  140. package/src/internal/machine/topology.ts +3 -3
  141. package/src/internal/machine/transition.ts +26 -0
  142. package/src/internal/testing/machine/finiteModel.ts +22 -23
  143. package/src/internal/testing/machine/format.ts +120 -0
  144. package/src/internal/testing/machine/probe.ts +4 -4
  145. package/src/internal/testing/machine/referenceModel.ts +25 -17
  146. package/src/internal/testing/machine/trace.ts +4 -4
  147. package/src/internal/testing/machine/value.ts +77 -0
  148. package/src/internal/testing/machine/verification.ts +904 -1109
  149. package/src/testing/MachineTest.ts +103 -114
  150. package/src/unstable/cluster/ClusterMachine.ts +10 -20
  151. package/src/unstable/reactivity/AtomMachine.ts +42 -79
@@ -15,20 +15,17 @@ import type {
15
15
  Coverage,
16
16
  CoverageSummary,
17
17
  EventCoverageItem,
18
- InitialTrace,
19
18
  Microstep,
20
19
  ObservedGraph,
21
20
  ObservedGraphEdge,
22
21
  ObservedGraphNode,
23
22
  PlanCompletion,
24
- RunFailure,
25
23
  Scenario,
26
24
  ScenarioOptions,
27
25
  Scenarios,
28
26
  SchemaArbitraryDiagnostic,
29
27
  StateCoverageItem,
30
28
  Trace,
31
- TraceStep,
32
29
  VerificationLaw,
33
30
  VerificationLawGroup,
34
31
  VerificationViolation,
@@ -38,117 +35,9 @@ import * as Protocol from "../../machine/protocol.js"
38
35
  import { toArbitraryWithReport } from "./arbitrary.js"
39
36
  import type { FiniteModel } from "./finiteModel.js"
40
37
  import * as ReferenceModel from "./referenceModel.js"
41
- import { rawConfigurationPaths, run } from "./trace.js"
38
+ import { rawConfigurationPaths } from "./trace.js"
42
39
  import { makeTransitionCoverageCollector, sameTransitionTrigger } from "./transitionCoverage.js"
43
-
44
- export {
45
- advanceCommand,
46
- type CausalRuntimeAssertionContext,
47
- type CausalRuntimeCommandActual,
48
- CausalRuntimeCommandFailure,
49
- type CausalRuntimeCommandRecord,
50
- type CausalRuntimeCommandResult,
51
- type CausalRuntimeInspectionContext,
52
- type CausalRuntimeModelOptions,
53
- type CausalRuntimeModelStep,
54
- type CausalRuntimeTranscript,
55
- type CausalVerificationAwaitContext,
56
- type CausalVerificationOptions,
57
- type CausalVerificationTranscript,
58
- checkpointCommand,
59
- type EnqueuedRuntimeAssertionContext,
60
- type EnqueuedRuntimeCommandActual,
61
- type EnqueuedRuntimeCommandRecord,
62
- type EnqueuedRuntimeInspectionContext,
63
- type EnqueuedRuntimeModelOptions,
64
- type EnqueuedRuntimeModelStep,
65
- type EnqueuedRuntimeTranscript,
66
- formatCausalTranscript,
67
- formatEnqueuedTranscript,
68
- formatRuntimeTranscript,
69
- runCausalCommands,
70
- runEnqueuedCommands,
71
- runRuntimeCommands,
72
- type RuntimeAssertionContext,
73
- type RuntimeAwait,
74
- type RuntimeCommand,
75
- type RuntimeCommandActual,
76
- RuntimeCommandFailure,
77
- type RuntimeCommandRecord,
78
- type RuntimeCommandResult,
79
- type RuntimeCommands,
80
- runtimeCommands,
81
- type RuntimeCommandsDiagnostics,
82
- type RuntimeCommandsOptions,
83
- type RuntimeInspectionContext,
84
- type RuntimeModelOptions,
85
- type RuntimeModelStep,
86
- RuntimeObservationError,
87
- RuntimeSynchronization,
88
- type RuntimeTranscript,
89
- sendCommand,
90
- stopCommand,
91
- verifyCausalCommands
92
- } from "./runtime.js"
93
-
94
- export type { SchemaArbitraryOpaqueFilterWarning, SchemaArbitraryReport, SchemaArbitraryWarning } from "./arbitrary.js"
95
-
96
- export {
97
- compileModel,
98
- type FiniteAtomicState,
99
- type FiniteAutomaticTransition,
100
- type FiniteCompoundState,
101
- type FiniteEventTransition,
102
- type FiniteFinalState,
103
- type FiniteHistoryMutation,
104
- type FiniteHistoryScenario,
105
- type FiniteHistoryState,
106
- type FiniteHistoryTransfer,
107
- type FiniteModel,
108
- type FiniteModelDiagnostics,
109
- type FiniteModelOptions,
110
- type FiniteModels,
111
- finiteModels,
112
- type FiniteParallelState,
113
- type FiniteState,
114
- type FiniteTransition,
115
- type FiniteTransitionTrigger
116
- } from "./finiteModel.js"
117
-
118
- export {
119
- ModelVerificationError,
120
- type ModelVerificationField,
121
- type ModelVerificationLocation,
122
- type ModelVerificationMismatch,
123
- type ReferenceCompletion,
124
- type ReferenceHistoryRecord,
125
- type ReferenceInitialStep,
126
- type ReferenceMicrostep,
127
- type ReferenceState,
128
- type ReferenceStateValue,
129
- type ReferenceStep,
130
- type ReferenceTrace,
131
- type ReferenceTransition
132
- } from "./referenceModel.js"
133
-
134
- export { assertInvariants, checkInvariants, Invariant, InvariantError, invariants } from "./invariant.js"
135
-
136
- export {
137
- assertPlannerRuntimeAgreement,
138
- assertRuntimeInvariants,
139
- checkRuntimeInvariants,
140
- PlannerRuntimeAgreementError,
141
- RuntimeInvariantError,
142
- runtimeInvariants
143
- } from "./runtimeInvariant.js"
144
-
145
- export { assertReachable, assertUnreachable, explore, findShortest, ReachabilityError } from "./exploration.js"
146
-
147
- export { probe, ProbeUnavailableError } from "./probe.js"
148
-
149
- export { run }
150
-
151
- export const interpretModel = ReferenceModel.interpretModel
40
+ import { sameValue } from "./value.js"
152
41
 
153
42
  type AnyMachine = Machine.Machine.Any
154
43
 
@@ -256,52 +145,6 @@ export const verifyModel = <M extends AnyMachine>(
256
145
  actualTrace: Trace<M>
257
146
  ): Effect.Effect<void, ReferenceModel.ModelVerificationError> => ReferenceModel.verifyModelTrace(model, actualTrace)
258
147
 
259
- const canonicalize = (value: unknown, active: WeakSet<object>): unknown => {
260
- if (value === undefined) return { $undefined: true }
261
- if (typeof value === "bigint") return { $bigint: String(value) }
262
- if (typeof value === "symbol") return { $symbol: String(value) }
263
- if (typeof value === "function") return { $function: value.name || "anonymous" }
264
- if (typeof value !== "object" || value === null) return value
265
- if (active.has(value)) return { $circular: true }
266
- active.add(value)
267
- let result: unknown
268
- if (value instanceof Error) {
269
- result = {
270
- $error: value.name,
271
- message: value.message,
272
- ...Object.fromEntries(
273
- Object.keys(value).sort().map((
274
- key
275
- ) => [key, canonicalize((value as unknown as Record<string, unknown>)[key], active)])
276
- )
277
- }
278
- } else if (Array.isArray(value)) {
279
- result = value.map((item) => canonicalize(item, active))
280
- } else if (value instanceof Date) {
281
- result = { $date: value.toISOString() }
282
- } else if (value instanceof Map) {
283
- result = {
284
- $map: Array.from(value, ([key, item]) => [canonicalize(key, active), canonicalize(item, active)]).sort((a, b) =>
285
- JSON.stringify(a).localeCompare(JSON.stringify(b))
286
- )
287
- }
288
- } else if (value instanceof Set) {
289
- result = {
290
- $set: Array.from(value, (item) => canonicalize(item, active)).sort((a, b) =>
291
- JSON.stringify(a).localeCompare(JSON.stringify(b))
292
- )
293
- }
294
- } else {
295
- result = Object.fromEntries(
296
- Object.keys(value).sort().map((key) => [key, canonicalize((value as Record<string, unknown>)[key], active)])
297
- )
298
- }
299
- active.delete(value)
300
- return result
301
- }
302
-
303
- const formatValue = (value: unknown): string => JSON.stringify(canonicalize(value, new WeakSet()))
304
-
305
148
  const structuralFingerprint = (value: unknown): string => {
306
149
  const references = new WeakMap<object, number>()
307
150
  let nextReference = 0
@@ -482,7 +325,7 @@ const targetWithinSelection = (
482
325
  return selection.kind === "state" &&
483
326
  (selection.scope === "local" || selection.scope === "branch") &&
484
327
  (selectedNode?.type === "compound" || selectedNode?.type === "parallel") &&
485
- target.startsWith(`${selection.path}.`)
328
+ (selection.path === "" || target.startsWith(`${selection.path}.`))
486
329
  }
487
330
 
488
331
  const finiteTagValues = (ast: SchemaAST.AST): ReadonlyArray<PropertyKey> | undefined => {
@@ -647,8 +490,8 @@ export const coverage = <M extends AnyMachine>(
647
490
  for (const trace of traces) {
648
491
  scenarioEvents += trace.scenario.events.length
649
492
  if (trace.scenario.events.length === 0) emptyScenarios += 1
650
- for (const event of trace.scenario.events) {
651
- const tag = event._tag
493
+ for (const step of trace.steps) {
494
+ const tag = step.event._tag
652
495
  eventCounts.set(tag, (eventCounts.get(tag) ?? 0) + 1)
653
496
  }
654
497
 
@@ -949,8 +792,6 @@ const isRecord = (value: unknown): value is Record<string, unknown> =>
949
792
 
950
793
  const hasOwn = (value: object, key: PropertyKey): boolean => Object.prototype.hasOwnProperty.call(value, key)
951
794
 
952
- const sameValue = (left: unknown, right: unknown): boolean => formatValue(left) === formatValue(right)
953
-
954
795
  const samePaths = (left: ReadonlyArray<string>, right: ReadonlyArray<string>): boolean =>
955
796
  left.length === right.length && left.every((path, index) => path === right[index])
956
797
 
@@ -995,1103 +836,1057 @@ export const verify = <M extends AnyMachine>(
995
836
  machine: M,
996
837
  trace: Trace<M>,
997
838
  options: VerifyOptions = {}
998
- ): Effect.Effect<void, VerificationError> => {
999
- const selected = new Set<VerificationLawGroup>(
1000
- options.laws ?? ["configuration", "microsteps", "completion", "history", "definitions"]
1001
- )
1002
- const nodes = Machine.stateNodes(machine) as ReadonlyArray<PublicStateNode>
1003
- const initialDefinition = Machine.initialDefinition(machine)
1004
- const definitions = Machine.transitionDefinitions(machine)
1005
- const { ancestors, byPath, depth, isDescendantOrSelf } = makeNodeUtilities(nodes)
1006
- const violations: Array<VerificationViolation> = []
1007
-
1008
- const add = (
1009
- law: VerificationLaw,
1010
- location: VerificationLocation,
1011
- message: string,
1012
- path?: string
1013
- ): void => {
1014
- violations.push({
1015
- law,
1016
- eventIndex: location.eventIndex,
1017
- ...(location.microstepIndex === undefined ? {} : { microstepIndex: location.microstepIndex }),
1018
- ...(path === undefined ? {} : { path }),
1019
- message
1020
- })
1021
- }
1022
-
1023
- const schemaMatches = (schema: Schema.Top | undefined, value: unknown): boolean => {
1024
- if (schema === undefined) return false
1025
- try {
1026
- return Schema.is(schema)(value)
1027
- } catch {
1028
- return false
839
+ ): Effect.Effect<void, VerificationError> =>
840
+ Effect.suspend(() => {
841
+ const selected = new Set<VerificationLawGroup>(
842
+ options.laws ?? ["configuration", "microsteps", "completion", "history", "definitions"]
843
+ )
844
+ const nodes = Machine.stateNodes(machine) as ReadonlyArray<PublicStateNode>
845
+ const initialDefinition = Machine.initialDefinition(machine)
846
+ const definitions = Machine.transitionDefinitions(machine)
847
+ const { ancestors, byPath, depth, isDescendantOrSelf } = makeNodeUtilities(nodes)
848
+ const violations: Array<VerificationViolation> = []
849
+
850
+ const add = (
851
+ law: VerificationLaw,
852
+ location: VerificationLocation,
853
+ message: string,
854
+ path?: string
855
+ ): void => {
856
+ violations.push({
857
+ law,
858
+ eventIndex: location.eventIndex,
859
+ ...(location.microstepIndex === undefined ? {} : { microstepIndex: location.microstepIndex }),
860
+ ...(path === undefined ? {} : { path }),
861
+ message
862
+ })
1029
863
  }
1030
- }
1031
864
 
1032
- const inspectSnapshot = (
1033
- snapshot: unknown,
1034
- location: VerificationLocation,
1035
- label: string
1036
- ): SnapshotInspection => {
1037
- const active = new Set<string>()
1038
- const paths: Array<string> = []
1039
- const values = new Map<string, unknown>()
1040
- const reportConfiguration = selected.has("configuration")
1041
- const root = isRecord(snapshot) ? snapshot : undefined
1042
-
1043
- const visit = (current: unknown, expectedParent: string | undefined, expectedPath?: string): void => {
1044
- if (!isRecord(current)) {
1045
- if (reportConfiguration) {
1046
- add("configuration.shape", location, `${label} must contain an object snapshot`)
1047
- }
1048
- return
1049
- }
1050
- if (typeof current.path !== "string") {
1051
- if (reportConfiguration) {
1052
- add("configuration.path", location, `${label} contains a snapshot without a string path`)
1053
- }
1054
- return
1055
- }
1056
- const path = current.path
1057
- if (active.has(path)) {
1058
- if (reportConfiguration) {
1059
- add("configuration.duplicate", location, `${label} activates state "${path}" more than once`, path)
1060
- }
1061
- return
865
+ const schemaMatches = (schema: Schema.Top | undefined, value: unknown): boolean => {
866
+ if (schema === undefined) return false
867
+ try {
868
+ return Schema.is(schema)(value)
869
+ } catch {
870
+ return false
1062
871
  }
1063
- active.add(path)
1064
- paths.push(path)
1065
- values.set(path, current.value)
872
+ }
1066
873
 
1067
- const node = byPath.get(path)
1068
- if (node === undefined) {
1069
- if (reportConfiguration) {
1070
- add("configuration.path", location, `${label} activates unknown state "${path}"`, path)
874
+ const inspectSnapshot = (
875
+ snapshot: unknown,
876
+ location: VerificationLocation,
877
+ label: string
878
+ ): SnapshotInspection => {
879
+ const active = new Set<string>()
880
+ const paths: Array<string> = []
881
+ const values = new Map<string, unknown>()
882
+ const reportConfiguration = selected.has("configuration")
883
+ const root = isRecord(snapshot) ? snapshot : undefined
884
+
885
+ const visit = (current: unknown, expectedParent: string | undefined, expectedPath?: string): void => {
886
+ if (!isRecord(current)) {
887
+ if (reportConfiguration) {
888
+ add("configuration.shape", location, `${label} must contain an object snapshot`)
889
+ }
890
+ return
1071
891
  }
1072
- if (isRecord(current.state)) visit(current.state, path)
1073
- if (isRecord(current.states)) {
1074
- for (const child of Object.values(current.states)) visit(child, path)
892
+ if (typeof current.path !== "string") {
893
+ if (reportConfiguration) {
894
+ add("configuration.path", location, `${label} contains a snapshot without a string path`)
895
+ }
896
+ return
1075
897
  }
1076
- return
1077
- }
1078
- if (expectedPath !== undefined && path !== expectedPath && reportConfiguration) {
1079
- add(
1080
- "configuration.hierarchy",
1081
- location,
1082
- `${label} expected region "${expectedPath}" but found "${path}"`,
1083
- path
1084
- )
1085
- }
1086
- if (node.parent !== expectedParent && reportConfiguration) {
1087
- add(
1088
- "configuration.hierarchy",
1089
- location,
1090
- expectedParent === undefined
1091
- ? `${label} root state "${path}" is not a machine root`
1092
- : `${label} state "${path}" is not a direct child of "${expectedParent}"`,
1093
- path
1094
- )
1095
- }
1096
- if (node.type === "history" || node.type === "choice") {
1097
- if (reportConfiguration) {
1098
- add("configuration.path", location, `${label} activates ${node.type} pseudo-state "${path}"`, path)
898
+ const path = current.path
899
+ if (active.has(path)) {
900
+ if (reportConfiguration) {
901
+ add("configuration.duplicate", location, `${label} activates state "${path}" more than once`, path)
902
+ }
903
+ return
1099
904
  }
1100
- return
1101
- }
1102
- if (reportConfiguration && node.schema === undefined && current.value !== undefined) {
1103
- add("configuration.schema", location, `${label} structural state "${path}" contains a value`, path)
1104
- } else if (reportConfiguration && node.schema !== undefined && !schemaMatches(node.schema, current.value)) {
1105
- add("configuration.schema", location, `${label} value for "${path}" does not match its schema`, path)
1106
- }
905
+ active.add(path)
906
+ paths.push(path)
907
+ values.set(path, current.value)
1107
908
 
1108
- if (node.type === "compound") {
1109
- if (!isRecord(current.state)) {
909
+ const node = byPath.get(path)
910
+ if (node === undefined) {
1110
911
  if (reportConfiguration) {
1111
- add(
1112
- "configuration.compound",
1113
- location,
1114
- `${label} compound state "${path}" must have exactly one child`,
1115
- path
1116
- )
912
+ add("configuration.path", location, `${label} activates unknown state "${path}"`, path)
1117
913
  }
1118
- } else {
1119
- visit(current.state, path)
914
+ if (isRecord(current.state)) visit(current.state, path)
915
+ if (isRecord(current.states)) {
916
+ for (const child of Object.values(current.states)) visit(child, path)
917
+ }
918
+ return
1120
919
  }
1121
- if (hasOwn(current, "states") && reportConfiguration) {
1122
- add("configuration.compound", location, `${label} compound state "${path}" contains parallel regions`, path)
920
+ if (expectedPath !== undefined && path !== expectedPath && reportConfiguration) {
921
+ add(
922
+ "configuration.hierarchy",
923
+ location,
924
+ `${label} expected region "${expectedPath}" but found "${path}"`,
925
+ path
926
+ )
1123
927
  }
1124
- return
1125
- }
1126
-
1127
- if (node.type === "parallel") {
1128
- if (!isRecord(current.states)) {
928
+ if (node.parent !== expectedParent && reportConfiguration) {
929
+ add(
930
+ "configuration.hierarchy",
931
+ location,
932
+ expectedParent === undefined
933
+ ? `${label} root state "${path}" is not a machine root`
934
+ : `${label} state "${path}" is not a direct child of "${expectedParent}"`,
935
+ path
936
+ )
937
+ }
938
+ if (node.type === "history" || node.type === "choice") {
1129
939
  if (reportConfiguration) {
1130
- add("configuration.parallel", location, `${label} parallel state "${path}" has no region map`, path)
940
+ add("configuration.path", location, `${label} activates ${node.type} pseudo-state "${path}"`, path)
1131
941
  }
1132
942
  return
1133
943
  }
1134
- const expectedKeys = new Set<string>()
1135
- for (const childPath of node.children) {
1136
- const child = byPath.get(childPath)
1137
- if (child === undefined) continue
1138
- expectedKeys.add(child.key)
1139
- if (!hasOwn(current.states, child.key)) {
944
+ if (reportConfiguration && node.schema === undefined && current.value !== undefined) {
945
+ add("configuration.schema", location, `${label} structural state "${path}" contains a value`, path)
946
+ } else if (reportConfiguration && node.schema !== undefined && !schemaMatches(node.schema, current.value)) {
947
+ add("configuration.schema", location, `${label} value for "${path}" does not match its schema`, path)
948
+ }
949
+
950
+ if (node.type === "compound") {
951
+ if (!isRecord(current.state)) {
1140
952
  if (reportConfiguration) {
1141
953
  add(
1142
- "configuration.parallel",
954
+ "configuration.compound",
1143
955
  location,
1144
- `${label} parallel state "${path}" omits region "${child.key}"`,
1145
- childPath
956
+ `${label} compound state "${path}" must have exactly one child`,
957
+ path
1146
958
  )
1147
959
  }
1148
960
  } else {
1149
- visit(current.states[child.key], path, child.path)
961
+ visit(current.state, path)
962
+ }
963
+ if (hasOwn(current, "states") && reportConfiguration) {
964
+ add("configuration.compound", location, `${label} compound state "${path}" contains parallel regions`, path)
1150
965
  }
966
+ return
1151
967
  }
1152
- for (const key of Object.keys(current.states)) {
1153
- if (!expectedKeys.has(key)) {
968
+
969
+ if (node.type === "parallel") {
970
+ if (!isRecord(current.states)) {
1154
971
  if (reportConfiguration) {
1155
- add(
1156
- "configuration.parallel",
1157
- location,
1158
- `${label} parallel state "${path}" contains extra region "${key}"`,
1159
- path
1160
- )
972
+ add("configuration.parallel", location, `${label} parallel state "${path}" has no region map`, path)
1161
973
  }
1162
- visit(current.states[key], path)
974
+ return
1163
975
  }
976
+ const expectedKeys = new Set<string>()
977
+ for (const childPath of node.children) {
978
+ const child = byPath.get(childPath)
979
+ if (child === undefined) continue
980
+ expectedKeys.add(child.key)
981
+ if (!hasOwn(current.states, child.key)) {
982
+ if (reportConfiguration) {
983
+ add(
984
+ "configuration.parallel",
985
+ location,
986
+ `${label} parallel state "${path}" omits region "${child.key}"`,
987
+ childPath
988
+ )
989
+ }
990
+ } else {
991
+ visit(current.states[child.key], path, child.path)
992
+ }
993
+ }
994
+ for (const key of Object.keys(current.states)) {
995
+ if (!expectedKeys.has(key)) {
996
+ if (reportConfiguration) {
997
+ add(
998
+ "configuration.parallel",
999
+ location,
1000
+ `${label} parallel state "${path}" contains extra region "${key}"`,
1001
+ path
1002
+ )
1003
+ }
1004
+ visit(current.states[key], path)
1005
+ }
1006
+ }
1007
+ if (hasOwn(current, "state") && reportConfiguration) {
1008
+ add("configuration.parallel", location, `${label} parallel state "${path}" contains a compound child`, path)
1009
+ }
1010
+ return
1164
1011
  }
1165
- if (hasOwn(current, "state") && reportConfiguration) {
1166
- add("configuration.parallel", location, `${label} parallel state "${path}" contains a compound child`, path)
1167
- }
1168
- return
1169
- }
1170
1012
 
1171
- if ((hasOwn(current, "state") || hasOwn(current, "states")) && reportConfiguration) {
1172
- add("configuration.shape", location, `${label} leaf state "${path}" contains active children`, path)
1013
+ if ((hasOwn(current, "state") || hasOwn(current, "states")) && reportConfiguration) {
1014
+ add("configuration.shape", location, `${label} leaf state "${path}" contains active children`, path)
1015
+ }
1173
1016
  }
1174
- }
1175
-
1176
- visit(snapshot, undefined)
1177
- return { active, paths, values, root }
1178
- }
1179
1017
 
1180
- const validateTraceConfiguration = (
1181
- expected: SnapshotInspection,
1182
- actual: ReadonlyArray<string>,
1183
- location: VerificationLocation,
1184
- label: string
1185
- ): void => {
1186
- if (selected.has("configuration") && !samePaths(expected.paths, actual)) {
1187
- add(
1188
- "configuration.trace",
1189
- location,
1190
- `${label} configuration [${actual.join(", ")}] does not match snapshot [${expected.paths.join(", ")}]`
1191
- )
1018
+ visit(snapshot, undefined)
1019
+ return { active, paths, values, root }
1192
1020
  }
1193
- }
1194
1021
 
1195
- const validateHistory = (
1196
- snapshot: SnapshotInspection,
1197
- location: VerificationLocation,
1198
- label: string
1199
- ): void => {
1200
- if (!selected.has("history") || snapshot.root === undefined || !hasOwn(snapshot.root, "history")) return
1201
- const history = snapshot.root.history
1202
- if (!isRecord(history)) {
1203
- add("history.record", location, `${label} history metadata must be a record`)
1204
- return
1205
- }
1206
- for (const [historyPath, unknownEntry] of Object.entries(history)) {
1207
- const historyNode = byPath.get(historyPath)
1208
- if (historyNode === undefined || historyNode.type !== "history" || historyNode.parent === undefined) {
1209
- add("history.path", location, `${label} contains unknown history record "${historyPath}"`, historyPath)
1210
- continue
1211
- }
1212
- if (!isRecord(unknownEntry)) {
1213
- add("history.record", location, `${label} history record "${historyPath}" must be an object`, historyPath)
1214
- continue
1215
- }
1216
- const entry = unknownEntry
1217
- if (entry.mode !== historyNode.history) {
1218
- add(
1219
- "history.mode",
1220
- location,
1221
- `${label} history record "${historyPath}" has mode "${
1222
- String(entry.mode)
1223
- }", expected "${historyNode.history}"`,
1224
- historyPath
1225
- )
1226
- }
1227
- if (!Array.isArray(entry.active) || !entry.active.every((path) => typeof path === "string")) {
1022
+ const validateTraceConfiguration = (
1023
+ expected: SnapshotInspection,
1024
+ actual: ReadonlyArray<string>,
1025
+ location: VerificationLocation,
1026
+ label: string
1027
+ ): void => {
1028
+ if (selected.has("configuration") && !samePaths(expected.paths, actual)) {
1228
1029
  add(
1229
- "history.record",
1030
+ "configuration.trace",
1230
1031
  location,
1231
- `${label} history record "${historyPath}" must contain string paths`,
1232
- historyPath
1032
+ `${label} configuration [${actual.join(", ")}] does not match snapshot [${expected.paths.join(", ")}]`
1233
1033
  )
1234
- continue
1235
- }
1236
- if (!isRecord(entry.values)) {
1237
- add(
1238
- "history.record",
1239
- location,
1240
- `${label} history record "${historyPath}" must contain a values record`,
1241
- historyPath
1242
- )
1243
- continue
1244
1034
  }
1035
+ }
1245
1036
 
1246
- const rememberedPaths = entry.active as ReadonlyArray<string>
1247
- const remembered = new Set<string>()
1248
- for (const path of rememberedPaths) {
1249
- if (remembered.has(path)) {
1250
- add("history.path", location, `${label} history record "${historyPath}" repeats "${path}"`, path)
1037
+ const validateHistory = (
1038
+ snapshot: SnapshotInspection,
1039
+ location: VerificationLocation,
1040
+ label: string
1041
+ ): void => {
1042
+ if (!selected.has("history") || snapshot.root === undefined || !hasOwn(snapshot.root, "history")) return
1043
+ const history = snapshot.root.history
1044
+ if (!isRecord(history)) {
1045
+ add("history.record", location, `${label} history metadata must be a record`)
1046
+ return
1047
+ }
1048
+ for (const [historyPath, unknownEntry] of Object.entries(history)) {
1049
+ const historyNode = byPath.get(historyPath)
1050
+ if (historyNode === undefined || historyNode.type !== "history" || historyNode.parent === undefined) {
1051
+ add("history.path", location, `${label} contains unknown history record "${historyPath}"`, historyPath)
1251
1052
  continue
1252
1053
  }
1253
- remembered.add(path)
1254
- const node = byPath.get(path)
1255
- if (node === undefined || node.type === "history" || node.type === "choice") {
1256
- add(
1257
- "history.path",
1258
- location,
1259
- `${label} history record "${historyPath}" contains invalid state "${path}"`,
1260
- path
1261
- )
1054
+ if (!isRecord(unknownEntry)) {
1055
+ add("history.record", location, `${label} history record "${historyPath}" must be an object`, historyPath)
1262
1056
  continue
1263
1057
  }
1264
- const inOwnerSubtree = isDescendantOrSelf(path, historyNode.parent)
1265
- const inOwnerAncestry = ancestors(historyNode.parent).includes(path)
1266
- if (!inOwnerSubtree && !inOwnerAncestry) {
1058
+ const entry = unknownEntry
1059
+ if (entry.mode !== historyNode.history) {
1267
1060
  add(
1268
- "history.path",
1061
+ "history.mode",
1269
1062
  location,
1270
- `${label} history record "${historyPath}" contains state "${path}" outside its owner`,
1271
- path
1063
+ `${label} history record "${historyPath}" has mode "${
1064
+ String(entry.mode)
1065
+ }", expected "${historyNode.history}"`,
1066
+ historyPath
1272
1067
  )
1273
1068
  }
1274
- const hasValue = hasOwn(entry.values, path)
1275
- if (node.schema === undefined && hasValue) {
1276
- add(
1277
- "history.value",
1278
- location,
1279
- `${label} history record "${historyPath}" values structural state "${path}"`,
1280
- path
1281
- )
1282
- } else if (node.schema !== undefined && !hasValue) {
1283
- add("history.value", location, `${label} history record "${historyPath}" omits value for "${path}"`, path)
1284
- } else if (node.schema !== undefined && !schemaMatches(node.schema, entry.values[path])) {
1069
+ if (!Array.isArray(entry.active) || !entry.active.every((path) => typeof path === "string")) {
1285
1070
  add(
1286
- "history.value",
1071
+ "history.record",
1287
1072
  location,
1288
- `${label} history value for "${path}" does not match its state schema`,
1289
- path
1073
+ `${label} history record "${historyPath}" must contain string paths`,
1074
+ historyPath
1290
1075
  )
1076
+ continue
1291
1077
  }
1292
- if (
1293
- entry.mode === "shallow" && isDescendantOrSelf(path, historyNode.parent) && path !== historyNode.parent &&
1294
- node.parent !== historyNode.parent
1295
- ) {
1078
+ if (!isRecord(entry.values)) {
1296
1079
  add(
1297
- "history.shallow",
1080
+ "history.record",
1298
1081
  location,
1299
- `${label} shallow history record "${historyPath}" contains deep descendant "${path}"`,
1300
- path
1082
+ `${label} history record "${historyPath}" must contain a values record`,
1083
+ historyPath
1301
1084
  )
1085
+ continue
1302
1086
  }
1303
- }
1304
- for (const path of Object.keys(entry.values)) {
1305
- if (!remembered.has(path)) {
1306
- add("history.value", location, `${label} history record "${historyPath}" has extra value "${path}"`, path)
1087
+
1088
+ const rememberedPaths = entry.active as ReadonlyArray<string>
1089
+ const remembered = new Set<string>()
1090
+ for (const path of rememberedPaths) {
1091
+ if (remembered.has(path)) {
1092
+ add("history.path", location, `${label} history record "${historyPath}" repeats "${path}"`, path)
1093
+ continue
1094
+ }
1095
+ remembered.add(path)
1096
+ const node = byPath.get(path)
1097
+ if (node === undefined || node.type === "history" || node.type === "choice") {
1098
+ add(
1099
+ "history.path",
1100
+ location,
1101
+ `${label} history record "${historyPath}" contains invalid state "${path}"`,
1102
+ path
1103
+ )
1104
+ continue
1105
+ }
1106
+ const inOwnerSubtree = isDescendantOrSelf(path, historyNode.parent)
1107
+ const inOwnerAncestry = ancestors(historyNode.parent).includes(path)
1108
+ if (!inOwnerSubtree && !inOwnerAncestry) {
1109
+ add(
1110
+ "history.path",
1111
+ location,
1112
+ `${label} history record "${historyPath}" contains state "${path}" outside its owner`,
1113
+ path
1114
+ )
1115
+ }
1116
+ const hasValue = hasOwn(entry.values, path)
1117
+ if (node.schema === undefined && hasValue) {
1118
+ add(
1119
+ "history.value",
1120
+ location,
1121
+ `${label} history record "${historyPath}" values structural state "${path}"`,
1122
+ path
1123
+ )
1124
+ } else if (node.schema !== undefined && !hasValue) {
1125
+ add("history.value", location, `${label} history record "${historyPath}" omits value for "${path}"`, path)
1126
+ } else if (node.schema !== undefined && !schemaMatches(node.schema, entry.values[path])) {
1127
+ add(
1128
+ "history.value",
1129
+ location,
1130
+ `${label} history value for "${path}" does not match its state schema`,
1131
+ path
1132
+ )
1133
+ }
1134
+ if (
1135
+ entry.mode === "shallow" && isDescendantOrSelf(path, historyNode.parent) && path !== historyNode.parent &&
1136
+ node.parent !== historyNode.parent
1137
+ ) {
1138
+ add(
1139
+ "history.shallow",
1140
+ location,
1141
+ `${label} shallow history record "${historyPath}" contains deep descendant "${path}"`,
1142
+ path
1143
+ )
1144
+ }
1307
1145
  }
1308
- }
1309
- if (entry.mode === "deep") {
1310
- for (const path of remembered) {
1311
- if (!isDescendantOrSelf(path, historyNode.parent) || path === historyNode.parent) continue
1312
- let parent: string | undefined = byPath.get(path)?.parent
1313
- while (parent !== undefined && isDescendantOrSelf(parent, historyNode.parent)) {
1314
- if (!remembered.has(parent)) {
1315
- add(
1316
- "history.deep",
1317
- location,
1318
- `${label} deep history record "${historyPath}" remembers "${path}" without ancestor "${parent}"`,
1319
- path
1320
- )
1321
- }
1322
- if (parent === historyNode.parent) break
1323
- parent = byPath.get(parent)?.parent
1146
+ for (const path of Object.keys(entry.values)) {
1147
+ if (!remembered.has(path)) {
1148
+ add("history.value", location, `${label} history record "${historyPath}" has extra value "${path}"`, path)
1324
1149
  }
1325
1150
  }
1326
- }
1327
- for (const ancestor of ancestors(historyNode.parent)) {
1328
- if (!remembered.has(ancestor)) {
1329
- add(
1330
- "history.path",
1331
- location,
1332
- `${label} history record "${historyPath}" omits owner ancestry state "${ancestor}"`,
1333
- ancestor
1334
- )
1151
+ if (entry.mode === "deep") {
1152
+ for (const path of remembered) {
1153
+ if (!isDescendantOrSelf(path, historyNode.parent) || path === historyNode.parent) continue
1154
+ let parent: string | undefined = byPath.get(path)?.parent
1155
+ while (parent !== undefined && isDescendantOrSelf(parent, historyNode.parent)) {
1156
+ if (!remembered.has(parent)) {
1157
+ add(
1158
+ "history.deep",
1159
+ location,
1160
+ `${label} deep history record "${historyPath}" remembers "${path}" without ancestor "${parent}"`,
1161
+ path
1162
+ )
1163
+ }
1164
+ if (parent === historyNode.parent) break
1165
+ parent = byPath.get(parent)?.parent
1166
+ }
1167
+ }
1335
1168
  }
1336
- }
1337
-
1338
- const validateRememberedControl = (path: string, recurse: boolean): void => {
1339
- const node = byPath.get(path)
1340
- if (node === undefined) return
1341
- const activeChildren = node.children.filter((child) => remembered.has(child))
1342
- if (node.type === "compound") {
1343
- if (activeChildren.length !== 1) {
1169
+ for (const ancestor of ancestors(historyNode.parent)) {
1170
+ if (!remembered.has(ancestor)) {
1344
1171
  add(
1345
- entry.mode === "deep" ? "history.deep" : "history.shallow",
1172
+ "history.path",
1346
1173
  location,
1347
- `${label} history record "${historyPath}" must remember one child of compound state "${path}"`,
1348
- path
1174
+ `${label} history record "${historyPath}" omits owner ancestry state "${ancestor}"`,
1175
+ ancestor
1349
1176
  )
1350
- } else if (recurse) {
1351
- validateRememberedControl(activeChildren[0]!, true)
1352
1177
  }
1353
- } else if (node.type === "parallel") {
1354
- for (const child of node.children) {
1355
- if (!remembered.has(child)) {
1178
+ }
1179
+
1180
+ const validateRememberedControl = (path: string, recurse: boolean): void => {
1181
+ const node = byPath.get(path)
1182
+ if (node === undefined) return
1183
+ const activeChildren = node.children.filter((child) => remembered.has(child))
1184
+ if (node.type === "compound") {
1185
+ if (activeChildren.length !== 1) {
1356
1186
  add(
1357
1187
  entry.mode === "deep" ? "history.deep" : "history.shallow",
1358
1188
  location,
1359
- `${label} history record "${historyPath}" omits parallel region "${child}"`,
1360
- child
1189
+ `${label} history record "${historyPath}" must remember one child of compound state "${path}"`,
1190
+ path
1361
1191
  )
1362
1192
  } else if (recurse) {
1363
- validateRememberedControl(child, true)
1193
+ validateRememberedControl(activeChildren[0]!, true)
1194
+ }
1195
+ } else if (node.type === "parallel") {
1196
+ for (const child of node.children) {
1197
+ if (!remembered.has(child)) {
1198
+ add(
1199
+ entry.mode === "deep" ? "history.deep" : "history.shallow",
1200
+ location,
1201
+ `${label} history record "${historyPath}" omits parallel region "${child}"`,
1202
+ child
1203
+ )
1204
+ } else if (recurse) {
1205
+ validateRememberedControl(child, true)
1206
+ }
1364
1207
  }
1365
1208
  }
1366
1209
  }
1210
+ validateRememberedControl(historyNode.parent, entry.mode === "deep")
1367
1211
  }
1368
- validateRememberedControl(historyNode.parent, entry.mode === "deep")
1369
1212
  }
1370
- }
1371
1213
 
1372
- const isCompletedControl = (path: string, active: ReadonlySet<string>): boolean => {
1373
- if (!active.has(path)) return false
1374
- const node = byPath.get(path)
1375
- if (node === undefined) return false
1376
- if (node.type === "final") return true
1377
- if (node.type === "compound") {
1378
- const child = node.children.find((candidate) => active.has(candidate))
1379
- return child !== undefined && byPath.get(child)?.type === "final"
1380
- }
1381
- if (node.type === "parallel") {
1382
- return node.children.length > 0 && node.children.every((child) => isCompletedControl(child, active))
1383
- }
1384
- return false
1385
- }
1386
-
1387
- const completionSchema = (path: string, active: ReadonlySet<string>): Schema.Top | undefined => {
1388
- const node = byPath.get(path)
1389
- if (node === undefined) return undefined
1390
- if (node.type === "compound") {
1391
- const child = node.children.find((candidate) => active.has(candidate))
1392
- return child === undefined ? undefined : completionSchema(child, active)
1214
+ const isCompletedControl = (path: string, active: ReadonlySet<string>): boolean => {
1215
+ if (!active.has(path)) return false
1216
+ const node = byPath.get(path)
1217
+ if (node === undefined) return false
1218
+ if (node.type === "final") return true
1219
+ if (node.type === "compound") {
1220
+ const child = node.children.find((candidate) => active.has(candidate))
1221
+ return child !== undefined && (path === ""
1222
+ ? !definitions.some((definition) => definition.source === child && definition.trigger.type === "done") &&
1223
+ isCompletedControl(child, active)
1224
+ : byPath.get(child)?.type === "final")
1225
+ }
1226
+ if (node.type === "parallel") {
1227
+ return node.children.length > 0 && node.children.every((child) => isCompletedControl(child, active))
1228
+ }
1229
+ return false
1393
1230
  }
1394
- return node.output ?? Schema.Void
1395
- }
1396
1231
 
1397
- const validateCompletions = (
1398
- snapshot: SnapshotInspection,
1399
- location: VerificationLocation,
1400
- label: string,
1401
- settled: boolean
1402
- ): ReadonlyMap<string, unknown> => {
1403
- const result = new Map<string, unknown>()
1404
- if (!selected.has("completion") || snapshot.root === undefined) {
1405
- return result
1406
- }
1407
- if (hasOwn(snapshot.root, "completed")) {
1408
- const completed = snapshot.root.completed
1409
- if (!Array.isArray(completed)) {
1410
- add("completion.record", location, `${label} completed metadata must be an array`)
1411
- } else {
1412
- for (const unknownEntry of completed) {
1413
- if (!isRecord(unknownEntry) || typeof unknownEntry.path !== "string") {
1414
- add("completion.record", location, `${label} contains an invalid completion record`)
1415
- continue
1416
- }
1417
- const path = unknownEntry.path
1418
- if (result.has(path)) {
1419
- add("completion.record", location, `${label} repeats completion "${path}"`, path)
1420
- continue
1421
- }
1422
- result.set(path, unknownEntry.output)
1423
- if (!snapshot.active.has(path) || !isCompletedControl(path, snapshot.active)) {
1424
- add("completion.record", location, `${label} completion "${path}" is not actively complete`, path)
1425
- continue
1426
- }
1427
- const schema = completionSchema(path, snapshot.active)
1428
- if (!schemaMatches(schema, unknownEntry.output)) {
1429
- add(
1430
- "completion.output",
1431
- location,
1432
- `${label} completion output for "${path}" does not match its schema`,
1433
- path
1434
- )
1232
+ const completionSchema = (path: string, active: ReadonlySet<string>): Schema.Top | undefined => {
1233
+ const node = byPath.get(path)
1234
+ if (node === undefined) return undefined
1235
+ if (node.type === "compound") {
1236
+ const child = node.children.find((candidate) => active.has(candidate))
1237
+ return child === undefined ? undefined : completionSchema(child, active)
1238
+ }
1239
+ return node.output ?? Schema.Void
1240
+ }
1241
+
1242
+ const validateCompletions = (
1243
+ snapshot: SnapshotInspection,
1244
+ location: VerificationLocation,
1245
+ label: string,
1246
+ settled: boolean
1247
+ ): ReadonlyMap<string, unknown> => {
1248
+ const result = new Map<string, unknown>()
1249
+ if (!selected.has("completion") || snapshot.root === undefined) {
1250
+ return result
1251
+ }
1252
+ if (hasOwn(snapshot.root, "completed")) {
1253
+ const completed = snapshot.root.completed
1254
+ if (!Array.isArray(completed)) {
1255
+ add("completion.record", location, `${label} completed metadata must be an array`)
1256
+ } else {
1257
+ for (const unknownEntry of completed) {
1258
+ if (!isRecord(unknownEntry) || typeof unknownEntry.path !== "string") {
1259
+ add("completion.record", location, `${label} contains an invalid completion record`)
1260
+ continue
1261
+ }
1262
+ const path = unknownEntry.path
1263
+ if (result.has(path)) {
1264
+ add("completion.record", location, `${label} repeats completion "${path}"`, path)
1265
+ continue
1266
+ }
1267
+ result.set(path, unknownEntry.output)
1268
+ if (!snapshot.active.has(path) || !isCompletedControl(path, snapshot.active)) {
1269
+ add("completion.record", location, `${label} completion "${path}" is not actively complete`, path)
1270
+ continue
1271
+ }
1272
+ const schema = completionSchema(path, snapshot.active)
1273
+ if (!schemaMatches(schema, unknownEntry.output)) {
1274
+ add(
1275
+ "completion.output",
1276
+ location,
1277
+ `${label} completion output for "${path}" does not match its schema`,
1278
+ path
1279
+ )
1280
+ }
1435
1281
  }
1436
1282
  }
1437
1283
  }
1438
- }
1439
- if (settled) {
1440
- for (const path of snapshot.paths) {
1441
- if (isCompletedControl(path, snapshot.active) && !result.has(path)) {
1442
- add("completion.record", location, `${label} omits settled completion "${path}"`, path)
1284
+ if (settled) {
1285
+ for (const path of snapshot.paths) {
1286
+ if (isCompletedControl(path, snapshot.active) && !result.has(path)) {
1287
+ add("completion.record", location, `${label} omits settled completion "${path}"`, path)
1288
+ }
1443
1289
  }
1444
1290
  }
1291
+ return result
1445
1292
  }
1446
- return result
1447
- }
1448
-
1449
- const validateSnapshotMetadata = (
1450
- snapshot: SnapshotInspection,
1451
- location: VerificationLocation,
1452
- label: string,
1453
- settled = false
1454
- ): ReadonlyMap<string, unknown> => {
1455
- validateHistory(snapshot, location, label)
1456
- return validateCompletions(snapshot, location, label, settled)
1457
- }
1458
1293
 
1459
- const sameControl = (left: SnapshotInspection, right: SnapshotInspection): boolean => {
1460
- if (!samePaths(left.paths, right.paths)) return false
1461
- for (const path of left.paths) {
1462
- if (!sameValue(left.values.get(path), right.values.get(path))) return false
1294
+ const validateSnapshotMetadata = (
1295
+ snapshot: SnapshotInspection,
1296
+ location: VerificationLocation,
1297
+ label: string,
1298
+ settled = false
1299
+ ): ReadonlyMap<string, unknown> => {
1300
+ validateHistory(snapshot, location, label)
1301
+ return validateCompletions(snapshot, location, label, settled)
1463
1302
  }
1464
- return sameValue(left.root?.history, right.root?.history)
1465
- }
1466
1303
 
1467
- const sameActivePaths = (left: SnapshotInspection, right: SnapshotInspection): boolean =>
1468
- left.active.size === right.active.size && Array.from(left.active).every((path) => right.active.has(path))
1304
+ const sameControl = (left: SnapshotInspection, right: SnapshotInspection): boolean => {
1305
+ if (!samePaths(left.paths, right.paths)) return false
1306
+ for (const path of left.paths) {
1307
+ if (!sameValue(left.values.get(path), right.values.get(path))) return false
1308
+ }
1309
+ return sameValue(left.root?.history, right.root?.history)
1310
+ }
1469
1311
 
1470
- const sortedPaths = (paths: ReadonlyArray<string>, direction: "entry" | "exit"): ReadonlyArray<string> =>
1471
- [...new Set(paths)].sort((left, right) => {
1472
- const depthDifference = direction === "entry" ? depth(left) - depth(right) : depth(right) - depth(left)
1473
- if (depthDifference !== 0) return depthDifference
1474
- const leftOrder = byPath.get(left)?.order ?? Number.MAX_SAFE_INTEGER
1475
- const rightOrder = byPath.get(right)?.order ?? Number.MAX_SAFE_INTEGER
1476
- return direction === "entry" ? leftOrder - rightOrder : rightOrder - leftOrder
1477
- })
1312
+ const sameActivePaths = (left: SnapshotInspection, right: SnapshotInspection): boolean =>
1313
+ left.active.size === right.active.size && Array.from(left.active).every((path) => right.active.has(path))
1478
1314
 
1479
- const transitionBranch = (
1480
- transition: Microstep<M>["transitions"][number]
1481
- ): Machine.Machine.TransitionBranch | undefined => {
1482
- const definition = definitions.find((candidate) =>
1483
- candidate.source === transition.source && candidate.reenter === transition.reenter &&
1484
- sameTransitionTrigger(candidate.trigger, transition.trigger)
1485
- )
1486
- return definition === undefined || !Number.isSafeInteger(transition.branchIndex) || transition.branchIndex < 0 ||
1487
- transition.branchIndex >= definition.branches.length
1488
- ? undefined
1489
- : definition.branches[transition.branchIndex]
1490
- }
1315
+ const sortedPaths = (paths: ReadonlyArray<string>, direction: "entry" | "exit"): ReadonlyArray<string> =>
1316
+ [...new Set(paths)].sort((left, right) => {
1317
+ const depthDifference = direction === "entry" ? depth(left) - depth(right) : depth(right) - depth(left)
1318
+ if (depthDifference !== 0) return depthDifference
1319
+ const leftOrder = byPath.get(left)?.order ?? Number.MAX_SAFE_INTEGER
1320
+ const rightOrder = byPath.get(right)?.order ?? Number.MAX_SAFE_INTEGER
1321
+ return direction === "entry" ? leftOrder - rightOrder : rightOrder - leftOrder
1322
+ })
1491
1323
 
1492
- const validateTransitionDefinition = (
1493
- transition: Microstep<M>["transitions"][number],
1494
- location: VerificationLocation
1495
- ): Machine.Machine.TransitionBranch | undefined => {
1496
- if (!selected.has("definitions")) return undefined
1497
- const definition = definitions.find((candidate) =>
1498
- candidate.source === transition.source && candidate.reenter === transition.reenter &&
1499
- sameTransitionTrigger(candidate.trigger, transition.trigger)
1500
- )
1501
- if (definition === undefined) {
1502
- add(
1503
- "definitions.transition",
1504
- location,
1505
- `retained transition from "${transition.source}" has no public definition`,
1506
- transition.source
1507
- )
1508
- return undefined
1509
- }
1510
- if (
1511
- !Number.isSafeInteger(transition.branchIndex) || transition.branchIndex < 0 ||
1512
- transition.branchIndex >= definition.branches.length
1513
- ) {
1514
- add(
1515
- "definitions.branchIndex",
1516
- location,
1517
- `retained transition from "${transition.source}" selected invalid branch index ${transition.branchIndex}`,
1518
- transition.source
1519
- )
1520
- return undefined
1521
- }
1522
- const branch = definition.branches[transition.branchIndex]!
1523
- const expectedBranchKey = branch.type === "branch" ? branch.key : undefined
1524
- if (transition.branchKey !== expectedBranchKey) {
1525
- add(
1526
- "definitions.branchKey",
1527
- location,
1528
- `retained transition from "${transition.source}" selected branch key ` +
1529
- `"${String(transition.branchKey)}" instead of "${String(expectedBranchKey)}"`,
1530
- transition.source
1324
+ const transitionBranch = (
1325
+ transition: Microstep<M>["transitions"][number]
1326
+ ): Machine.Machine.TransitionBranch | undefined => {
1327
+ const definition = definitions.find((candidate) =>
1328
+ candidate.source === transition.source && candidate.reenter === transition.reenter &&
1329
+ sameTransitionTrigger(candidate.trigger, transition.trigger)
1531
1330
  )
1532
- return undefined
1533
- }
1534
- if (!targetWithinSelection(transition.target, branch, byPath)) {
1535
- const expected = branch.selection.kind === "none"
1536
- ? "an explicitly targetless result"
1537
- : `selection ${branch.selection.kind}:${branch.selection.scope}:${String(branch.selection.path)}`
1538
- add(
1539
- "definitions.selection",
1540
- location,
1541
- `transition branch ${transition.branchIndex} from "${transition.source}" returned ` +
1542
- `target "${String(transition.target)}" outside ${expected}`,
1543
- transition.target === undefined ? transition.source : String(transition.target)
1331
+ return definition === undefined || !Number.isSafeInteger(transition.branchIndex) || transition.branchIndex < 0 ||
1332
+ transition.branchIndex >= definition.branches.length
1333
+ ? undefined
1334
+ : definition.branches[transition.branchIndex]
1335
+ }
1336
+
1337
+ const validateTransitionDefinition = (
1338
+ transition: Microstep<M>["transitions"][number],
1339
+ location: VerificationLocation
1340
+ ): Machine.Machine.TransitionBranch | undefined => {
1341
+ if (!selected.has("definitions")) return undefined
1342
+ const definition = definitions.find((candidate) =>
1343
+ candidate.source === transition.source && candidate.reenter === transition.reenter &&
1344
+ sameTransitionTrigger(candidate.trigger, transition.trigger)
1544
1345
  )
1545
- }
1546
- if (
1547
- transition.updates.length !== branch.updates.length ||
1548
- transition.updates.some((path, index) => path !== branch.updates[index])
1549
- ) {
1550
- add(
1551
- "definitions.selection",
1552
- location,
1553
- `transition branch ${transition.branchIndex} from "${transition.source}" reported retained owner updates ` +
1554
- `${JSON.stringify(transition.updates)} instead of ${JSON.stringify(branch.updates)}`,
1555
- transition.source
1556
- )
1557
- }
1558
- return branch
1559
- }
1560
-
1561
- const validateTransitionResolutions = (
1562
- transitions: Microstep<M>["transitions"],
1563
- branches: ReadonlyArray<Machine.Machine.TransitionBranch | undefined>,
1564
- location: VerificationLocation
1565
- ): void => {
1566
- if (!selected.has("definitions")) return
1567
-
1568
- const choiceResolution = (
1569
- start: string,
1570
- afterIndex: number,
1571
- nested: boolean
1572
- ): { readonly found: boolean; readonly target: string | undefined } => {
1573
- const seen = new Set<string>()
1574
- let choice = start
1575
- let cursor = afterIndex + 1
1576
- let first = true
1577
- while (!seen.has(choice)) {
1578
- seen.add(choice)
1579
- let choiceIndex = -1
1580
- for (let index = cursor; index < transitions.length; index++) {
1581
- const candidate = transitions[index]!
1582
- if (
1583
- candidate.trigger.type === "choice" &&
1584
- (candidate.source === choice || first && nested && isDescendantOrSelf(String(candidate.source), choice))
1585
- ) {
1586
- choiceIndex = index
1587
- break
1346
+ if (definition === undefined) {
1347
+ add(
1348
+ "definitions.transition",
1349
+ location,
1350
+ `retained transition from "${transition.source}" has no public definition`,
1351
+ transition.source
1352
+ )
1353
+ return undefined
1354
+ }
1355
+ if (
1356
+ !Number.isSafeInteger(transition.branchIndex) || transition.branchIndex < 0 ||
1357
+ transition.branchIndex >= definition.branches.length
1358
+ ) {
1359
+ add(
1360
+ "definitions.branchIndex",
1361
+ location,
1362
+ `retained transition from "${transition.source}" selected invalid branch index ${transition.branchIndex}`,
1363
+ transition.source
1364
+ )
1365
+ return undefined
1366
+ }
1367
+ const branch = definition.branches[transition.branchIndex]!
1368
+ const expectedBranchKey = branch.type === "branch" ? branch.key : undefined
1369
+ if (transition.branchKey !== expectedBranchKey) {
1370
+ add(
1371
+ "definitions.branchKey",
1372
+ location,
1373
+ `retained transition from "${transition.source}" selected branch key ` +
1374
+ `"${String(transition.branchKey)}" instead of "${String(expectedBranchKey)}"`,
1375
+ transition.source
1376
+ )
1377
+ return undefined
1378
+ }
1379
+ if (!targetWithinSelection(transition.target, branch, byPath)) {
1380
+ const expected = branch.selection.kind === "none"
1381
+ ? "an explicitly targetless result"
1382
+ : `selection ${branch.selection.kind}:${branch.selection.scope}:${String(branch.selection.path)}`
1383
+ add(
1384
+ "definitions.selection",
1385
+ location,
1386
+ `transition branch ${transition.branchIndex} from "${transition.source}" returned ` +
1387
+ `target "${String(transition.target)}" outside ${expected}`,
1388
+ transition.target === undefined ? transition.source : String(transition.target)
1389
+ )
1390
+ }
1391
+ if (
1392
+ transition.updates.length !== branch.updates.length ||
1393
+ transition.updates.some((path, index) => path !== branch.updates[index])
1394
+ ) {
1395
+ add(
1396
+ "definitions.selection",
1397
+ location,
1398
+ `transition branch ${transition.branchIndex} from "${transition.source}" reported retained owner updates ` +
1399
+ `${JSON.stringify(transition.updates)} instead of ${JSON.stringify(branch.updates)}`,
1400
+ transition.source
1401
+ )
1402
+ }
1403
+ return branch
1404
+ }
1405
+
1406
+ const validateTransitionResolutions = (
1407
+ transitions: Microstep<M>["transitions"],
1408
+ branches: ReadonlyArray<Machine.Machine.TransitionBranch | undefined>,
1409
+ location: VerificationLocation
1410
+ ): void => {
1411
+ if (!selected.has("definitions")) return
1412
+
1413
+ const choiceResolution = (
1414
+ start: string,
1415
+ afterIndex: number,
1416
+ nested: boolean
1417
+ ): { readonly found: boolean; readonly target: string | undefined } => {
1418
+ const seen = new Set<string>()
1419
+ let choice = start
1420
+ let cursor = afterIndex + 1
1421
+ let first = true
1422
+ while (!seen.has(choice)) {
1423
+ seen.add(choice)
1424
+ let choiceIndex = -1
1425
+ for (let index = cursor; index < transitions.length; index++) {
1426
+ const candidate = transitions[index]!
1427
+ if (
1428
+ candidate.trigger.type === "choice" &&
1429
+ (candidate.source === choice || first && nested && isDescendantOrSelf(String(candidate.source), choice))
1430
+ ) {
1431
+ choiceIndex = index
1432
+ break
1433
+ }
1434
+ }
1435
+ if (choiceIndex === -1) return { found: false, target: undefined }
1436
+ const transition = transitions[choiceIndex]!
1437
+ if (branches[choiceIndex] === undefined) return { found: false, target: undefined }
1438
+ const target = transition.target === undefined ? undefined : String(transition.target)
1439
+ if (target === undefined) return { found: true, target: undefined }
1440
+ const targetNode = byPath.get(target)
1441
+ if (targetNode?.type === "choice") {
1442
+ choice = target
1443
+ cursor = choiceIndex + 1
1444
+ first = false
1445
+ continue
1446
+ }
1447
+ return {
1448
+ found: true,
1449
+ target: targetNode?.type === "history" ? targetNode.parent : target
1588
1450
  }
1589
1451
  }
1590
- if (choiceIndex === -1) return { found: false, target: undefined }
1591
- const transition = transitions[choiceIndex]!
1592
- if (branches[choiceIndex] === undefined) return { found: false, target: undefined }
1452
+ return { found: false, target: undefined }
1453
+ }
1454
+
1455
+ transitions.forEach((transition, index) => {
1456
+ if (branches[index] === undefined) return
1593
1457
  const target = transition.target === undefined ? undefined : String(transition.target)
1594
- if (target === undefined) return { found: true, target: undefined }
1595
- const targetNode = byPath.get(target)
1596
- if (targetNode?.type === "choice") {
1597
- choice = target
1598
- cursor = choiceIndex + 1
1599
- first = false
1600
- continue
1601
- }
1602
- return {
1603
- found: true,
1604
- target: targetNode?.type === "history" ? targetNode.parent : target
1458
+ const resolvedTarget = transition.resolvedTarget === undefined ? undefined : String(transition.resolvedTarget)
1459
+ const targetNode = target === undefined ? undefined : byPath.get(target)
1460
+ let expected = target
1461
+ let explanation = transition.updates.length > 0
1462
+ ? target === undefined
1463
+ ? `update owner "${transition.updates.join("\", \"")}"`
1464
+ : `target "${target}" and update owner "${transition.updates.join("\", \"")}"`
1465
+ : target === undefined
1466
+ ? "an unresolved targetless transition"
1467
+ : `target "${target}"`
1468
+
1469
+ if (transition.trigger.type === "choice") {
1470
+ explanation = target === undefined ? "a targetless choice edge" : `choice edge target "${target}"`
1471
+ } else if (targetNode?.type === "history") {
1472
+ expected = targetNode.parent
1473
+ explanation = `history owner "${String(targetNode.parent)}"`
1474
+ } else if (targetNode?.type === "choice") {
1475
+ const resolution = choiceResolution(targetNode.path, index, false)
1476
+ if (!resolution.found) {
1477
+ add(
1478
+ "definitions.resolution",
1479
+ location,
1480
+ `transition from "${transition.source}" targets choice "${target}" without an exact retained route`,
1481
+ target
1482
+ )
1483
+ return
1484
+ }
1485
+ expected = resolution.target
1486
+ explanation = expected === undefined ?
1487
+ `targetless route from choice "${target}"` :
1488
+ `choice route from "${target}" to "${expected}"`
1489
+ } else if (
1490
+ resolvedTarget !== target &&
1491
+ (targetNode?.type === "compound" || targetNode?.type === "parallel")
1492
+ ) {
1493
+ const resolution = choiceResolution(targetNode.path, index, true)
1494
+ if (resolution.found) {
1495
+ expected = resolution.target
1496
+ explanation = expected === undefined ?
1497
+ `targetless nested choice route from "${target}"` :
1498
+ `nested choice route from "${target}" to "${expected}"`
1499
+ }
1605
1500
  }
1606
- }
1607
- return { found: false, target: undefined }
1608
- }
1609
1501
 
1610
- transitions.forEach((transition, index) => {
1611
- if (branches[index] === undefined) return
1612
- const target = transition.target === undefined ? undefined : String(transition.target)
1613
- const resolvedTarget = transition.resolvedTarget === undefined ? undefined : String(transition.resolvedTarget)
1614
- const targetNode = target === undefined ? undefined : byPath.get(target)
1615
- let expected = target
1616
- let explanation = transition.updates.length > 0
1617
- ? target === undefined
1618
- ? `update owner "${transition.updates.join("\", \"")}"`
1619
- : `target "${target}" and update owner "${transition.updates.join("\", \"")}"`
1620
- : target === undefined
1621
- ? "an unresolved targetless transition"
1622
- : `target "${target}"`
1623
-
1624
- if (transition.trigger.type === "choice") {
1625
- explanation = target === undefined ? "a targetless choice edge" : `choice edge target "${target}"`
1626
- } else if (targetNode?.type === "history") {
1627
- expected = targetNode.parent
1628
- explanation = `history owner "${String(targetNode.parent)}"`
1629
- } else if (targetNode?.type === "choice") {
1630
- const resolution = choiceResolution(targetNode.path, index, false)
1631
- if (!resolution.found) {
1502
+ if (resolvedTarget !== expected) {
1632
1503
  add(
1633
1504
  "definitions.resolution",
1634
1505
  location,
1635
- `transition from "${transition.source}" targets choice "${target}" without an exact retained route`,
1636
- target
1506
+ `transition branch ${transition.branchIndex} from "${transition.source}" resolved to ` +
1507
+ `"${String(transition.resolvedTarget)}" instead of ${explanation}`,
1508
+ resolvedTarget ?? target ?? String(transition.source)
1637
1509
  )
1638
- return
1639
1510
  }
1640
- expected = resolution.target
1641
- explanation = expected === undefined ?
1642
- `targetless route from choice "${target}"` :
1643
- `choice route from "${target}" to "${expected}"`
1644
- } else if (
1645
- resolvedTarget !== target &&
1646
- (targetNode?.type === "compound" || targetNode?.type === "parallel")
1647
- ) {
1648
- const resolution = choiceResolution(targetNode.path, index, true)
1649
- if (resolution.found) {
1650
- expected = resolution.target
1651
- explanation = expected === undefined ?
1652
- `targetless nested choice route from "${target}"` :
1653
- `nested choice route from "${target}" to "${expected}"`
1654
- }
1655
- }
1656
-
1657
- if (resolvedTarget !== expected) {
1658
- add(
1659
- "definitions.resolution",
1660
- location,
1661
- `transition branch ${transition.branchIndex} from "${transition.source}" resolved to ` +
1662
- `"${String(transition.resolvedTarget)}" instead of ${explanation}`,
1663
- resolvedTarget ?? target ?? String(transition.source)
1664
- )
1665
- }
1666
- })
1667
- }
1668
-
1669
- const initialChoiceRouteRoot = (): string | undefined => {
1670
- const routing = trace.initial.plan.microsteps[0]
1671
- if (
1672
- routing === undefined || routing.changed ||
1673
- routing.transitions.length === 0 ||
1674
- !routing.transitions.every((transition) => transition.trigger.type === "choice")
1675
- ) {
1676
- return undefined
1511
+ })
1677
1512
  }
1678
1513
 
1679
- const reachableScopes: Array<string> = [initialDefinition.target]
1680
- let terminalRoot: string | undefined
1681
- for (const transition of routing.transitions) {
1682
- const source = String(transition.source)
1683
- const branch = transitionBranch(transition)
1514
+ const initialChoiceRouteRoot = (): string | undefined => {
1515
+ const routing = trace.initial.plan.microsteps[0]
1684
1516
  if (
1685
- branch === undefined || !targetWithinSelection(transition.target, branch, byPath) ||
1686
- !reachableScopes.some((scope) => isDescendantOrSelf(source, scope))
1517
+ routing === undefined || routing.changed ||
1518
+ routing.transitions.length === 0 ||
1519
+ !routing.transitions.every((transition) => transition.trigger.type === "choice")
1687
1520
  ) {
1688
1521
  return undefined
1689
1522
  }
1690
- const target = transition.target === undefined ? undefined : String(transition.target)
1691
- if (target === undefined) return undefined
1692
- const targetNode = byPath.get(target)
1693
- const scope = targetNode?.type === "history" ? targetNode.parent : target
1694
- if (scope === undefined) return undefined
1695
- reachableScopes.push(scope)
1696
- terminalRoot = ancestors(scope)[0]
1697
- }
1698
- return terminalRoot
1699
- }
1700
1523
 
1701
- const validateMicrostep = (
1702
- microstep: Microstep<M, any>,
1703
- before: SnapshotInspection,
1704
- after: SnapshotInspection,
1705
- location: VerificationLocation
1706
- ): void => {
1707
- if (selected.has("microsteps")) {
1708
- const uniqueExit = new Set(microstep.exitPaths)
1709
- const uniqueEntry = new Set(microstep.entryPaths)
1710
- const reentering = microstep.transitions.filter((transition) => transition.reenter)
1711
- const inReentryScope = (
1712
- path: string,
1713
- transition: Microstep<M>["transitions"][number]
1714
- ): boolean => {
1715
- const target = transition.target === undefined ? undefined : byPath.get(String(transition.target))
1716
- if (target?.type === "history" && target.parent !== undefined && before.active.has(target.parent)) {
1717
- return isDescendantOrSelf(path, target.parent)
1524
+ const reachableScopes: Array<string> = [initialDefinition.target]
1525
+ let terminalRoot: string | undefined
1526
+ for (const transition of routing.transitions) {
1527
+ const source = String(transition.source)
1528
+ const branch = transitionBranch(transition)
1529
+ if (
1530
+ branch === undefined || !targetWithinSelection(transition.target, branch, byPath) ||
1531
+ !reachableScopes.some((scope) => isDescendantOrSelf(source, scope))
1532
+ ) {
1533
+ return undefined
1718
1534
  }
1719
- const parent = byPath.get(String(transition.source))?.parent
1720
- return parent === undefined || path !== parent && isDescendantOrSelf(path, parent)
1535
+ const target = transition.target === undefined ? undefined : String(transition.target)
1536
+ if (target === undefined) return undefined
1537
+ const targetNode = byPath.get(target)
1538
+ const scope = targetNode?.type === "history" ? targetNode.parent : target
1539
+ if (scope === undefined) return undefined
1540
+ reachableScopes.push(scope)
1541
+ terminalRoot = ancestors(scope)[0]
1721
1542
  }
1722
- const commonLifecycleExplained = (path: string): boolean =>
1723
- microstep.transitions.some((transition) => {
1724
- if (transition.reenter) {
1725
- const target = transition.target === undefined ? undefined : byPath.get(String(transition.target))
1726
- if (target?.type === "history" && target.parent !== undefined && before.active.has(target.parent)) {
1727
- // Reentry into an active history owner has a dedicated boundary:
1728
- // only the owner subtree is exited and entered, regardless of
1729
- // the ordinary source/resolved-target LCA.
1730
- return isDescendantOrSelf(path, target.parent)
1543
+ return terminalRoot
1544
+ }
1545
+
1546
+ const validateMicrostep = (
1547
+ microstep: Microstep<M, any>,
1548
+ before: SnapshotInspection,
1549
+ after: SnapshotInspection,
1550
+ location: VerificationLocation
1551
+ ): void => {
1552
+ if (selected.has("microsteps")) {
1553
+ const uniqueExit = new Set(microstep.exitPaths)
1554
+ const uniqueEntry = new Set(microstep.entryPaths)
1555
+ const reentering = microstep.transitions.filter((transition) => transition.reenter)
1556
+ const inReentryScope = (
1557
+ path: string,
1558
+ transition: Microstep<M>["transitions"][number]
1559
+ ): boolean => {
1560
+ const target = transition.target === undefined ? undefined : byPath.get(String(transition.target))
1561
+ if (target?.type === "history" && target.parent !== undefined && before.active.has(target.parent)) {
1562
+ return isDescendantOrSelf(path, target.parent)
1563
+ }
1564
+ const parent = byPath.get(String(transition.source))?.parent
1565
+ return parent === undefined || path !== parent && isDescendantOrSelf(path, parent)
1566
+ }
1567
+ const commonLifecycleExplained = (path: string): boolean =>
1568
+ microstep.transitions.some((transition) => {
1569
+ if (transition.reenter) {
1570
+ const target = transition.target === undefined ? undefined : byPath.get(String(transition.target))
1571
+ if (target?.type === "history" && target.parent !== undefined && before.active.has(target.parent)) {
1572
+ // Reentry into an active history owner has a dedicated boundary:
1573
+ // only the owner subtree is exited and entered, regardless of
1574
+ // the ordinary source/resolved-target LCA.
1575
+ return isDescendantOrSelf(path, target.parent)
1576
+ }
1577
+ if (inReentryScope(path, transition)) return true
1731
1578
  }
1732
- if (inReentryScope(path, transition)) return true
1579
+ if (transition.resolvedTarget === undefined) return false
1580
+ const sourceAncestors = ancestors(String(transition.source))
1581
+ const targetAncestors = ancestors(String(transition.resolvedTarget))
1582
+ let boundary: string | undefined
1583
+ for (let index = 0; index < Math.min(sourceAncestors.length, targetAncestors.length); index++) {
1584
+ if (sourceAncestors[index] !== targetAncestors[index]) break
1585
+ boundary = sourceAncestors[index]
1586
+ }
1587
+ return boundary === undefined || path !== boundary && isDescendantOrSelf(path, boundary)
1588
+ })
1589
+ if (uniqueExit.size !== microstep.exitPaths.length) {
1590
+ add("microsteps.unique", location, "microstep exit paths contain duplicates")
1591
+ }
1592
+ if (uniqueEntry.size !== microstep.entryPaths.length) {
1593
+ add("microsteps.unique", location, "microstep entry paths contain duplicates")
1594
+ }
1595
+ if (!samePaths(microstep.exitPaths, sortedPaths(microstep.exitPaths, "exit"))) {
1596
+ add("microsteps.order", location, "microstep exit paths are not deepest-first in reverse document order")
1597
+ }
1598
+ if (!samePaths(microstep.entryPaths, sortedPaths(microstep.entryPaths, "entry"))) {
1599
+ add("microsteps.order", location, "microstep entry paths are not parent-first in document order")
1600
+ }
1601
+ for (const path of microstep.exitPaths) {
1602
+ if (!before.active.has(path)) {
1603
+ add("microsteps.activeBefore", location, `microstep exits inactive state "${path}"`, path)
1733
1604
  }
1734
- if (transition.resolvedTarget === undefined) return false
1735
- const sourceAncestors = ancestors(String(transition.source))
1736
- const targetAncestors = ancestors(String(transition.resolvedTarget))
1737
- let boundary: string | undefined
1738
- for (let index = 0; index < Math.min(sourceAncestors.length, targetAncestors.length); index++) {
1739
- if (sourceAncestors[index] !== targetAncestors[index]) break
1740
- boundary = sourceAncestors[index]
1605
+ }
1606
+ for (const path of microstep.entryPaths) {
1607
+ if (!after.active.has(path)) {
1608
+ add("microsteps.activeAfter", location, `microstep enters state "${path}" absent from its next state`, path)
1741
1609
  }
1742
- return boundary === undefined || path !== boundary && isDescendantOrSelf(path, boundary)
1743
- })
1744
- if (uniqueExit.size !== microstep.exitPaths.length) {
1745
- add("microsteps.unique", location, "microstep exit paths contain duplicates")
1746
- }
1747
- if (uniqueEntry.size !== microstep.entryPaths.length) {
1748
- add("microsteps.unique", location, "microstep entry paths contain duplicates")
1749
- }
1750
- if (!samePaths(microstep.exitPaths, sortedPaths(microstep.exitPaths, "exit"))) {
1751
- add("microsteps.order", location, "microstep exit paths are not deepest-first in reverse document order")
1752
- }
1753
- if (!samePaths(microstep.entryPaths, sortedPaths(microstep.entryPaths, "entry"))) {
1754
- add("microsteps.order", location, "microstep entry paths are not parent-first in document order")
1755
- }
1756
- for (const path of microstep.exitPaths) {
1757
- if (!before.active.has(path)) {
1758
- add("microsteps.activeBefore", location, `microstep exits inactive state "${path}"`, path)
1759
1610
  }
1760
- }
1761
- for (const path of microstep.entryPaths) {
1762
- if (!after.active.has(path)) {
1763
- add("microsteps.activeAfter", location, `microstep enters state "${path}" absent from its next state`, path)
1611
+ for (const path of before.paths) {
1612
+ if (!after.active.has(path) && !uniqueExit.has(path)) {
1613
+ add("microsteps.activeBefore", location, `removed state "${path}" is missing from exit paths`, path)
1614
+ }
1764
1615
  }
1765
- }
1766
- for (const path of before.paths) {
1767
- if (!after.active.has(path) && !uniqueExit.has(path)) {
1768
- add("microsteps.activeBefore", location, `removed state "${path}" is missing from exit paths`, path)
1616
+ for (const path of after.paths) {
1617
+ if (!before.active.has(path) && !uniqueEntry.has(path)) {
1618
+ add("microsteps.activeAfter", location, `added state "${path}" is missing from entry paths`, path)
1619
+ }
1769
1620
  }
1770
- }
1771
- for (const path of after.paths) {
1772
- if (!before.active.has(path) && !uniqueEntry.has(path)) {
1773
- add("microsteps.activeAfter", location, `added state "${path}" is missing from entry paths`, path)
1621
+ for (const transition of reentering) {
1622
+ for (const path of before.paths) {
1623
+ if (inReentryScope(path, transition) && !uniqueExit.has(path)) {
1624
+ add(
1625
+ "microsteps.reentry",
1626
+ location,
1627
+ `reentering transition from "${String(transition.source)}" omits exit lifecycle for "${path}"`,
1628
+ path
1629
+ )
1630
+ }
1631
+ }
1632
+ for (const path of after.paths) {
1633
+ if (inReentryScope(path, transition) && !uniqueEntry.has(path)) {
1634
+ add(
1635
+ "microsteps.reentry",
1636
+ location,
1637
+ `reentering transition from "${String(transition.source)}" omits entry lifecycle for "${path}"`,
1638
+ path
1639
+ )
1640
+ }
1641
+ }
1774
1642
  }
1775
- }
1776
- for (const transition of reentering) {
1777
1643
  for (const path of before.paths) {
1778
- if (inReentryScope(path, transition) && !uniqueExit.has(path)) {
1644
+ if (!after.active.has(path) || !uniqueExit.has(path) && !uniqueEntry.has(path)) continue
1645
+ if (!commonLifecycleExplained(path)) {
1779
1646
  add(
1780
1647
  "microsteps.reentry",
1781
1648
  location,
1782
- `reentering transition from "${String(transition.source)}" omits exit lifecycle for "${path}"`,
1649
+ `common state "${path}" has lifecycle without a reentering transition`,
1783
1650
  path
1784
1651
  )
1785
- }
1786
- }
1787
- for (const path of after.paths) {
1788
- if (inReentryScope(path, transition) && !uniqueEntry.has(path)) {
1652
+ } else if (!uniqueExit.has(path) || !uniqueEntry.has(path)) {
1789
1653
  add(
1790
1654
  "microsteps.reentry",
1791
1655
  location,
1792
- `reentering transition from "${String(transition.source)}" omits entry lifecycle for "${path}"`,
1656
+ `reentered common state "${path}" must have both exit and entry lifecycle`,
1793
1657
  path
1794
1658
  )
1795
1659
  }
1796
1660
  }
1797
- }
1798
- for (const path of before.paths) {
1799
- if (!after.active.has(path) || !uniqueExit.has(path) && !uniqueEntry.has(path)) continue
1800
- if (!commonLifecycleExplained(path)) {
1801
- add(
1802
- "microsteps.reentry",
1803
- location,
1804
- `common state "${path}" has lifecycle without a reentering transition`,
1805
- path
1806
- )
1807
- } else if (!uniqueExit.has(path) || !uniqueEntry.has(path)) {
1808
- add(
1809
- "microsteps.reentry",
1810
- location,
1811
- `reentered common state "${path}" must have both exit and entry lifecycle`,
1812
- path
1813
- )
1814
- }
1815
- }
1816
- if (!microstep.changed) {
1817
- if (microstep.exitPaths.length > 0 || microstep.entryPaths.length > 0) {
1818
- add("microsteps.changed", location, "unchanged microstep contains entry or exit paths")
1661
+ if (!microstep.changed) {
1662
+ if (microstep.exitPaths.length > 0 || microstep.entryPaths.length > 0) {
1663
+ add("microsteps.changed", location, "unchanged microstep contains entry or exit paths")
1664
+ }
1665
+ if (!sameActivePaths(before, after)) {
1666
+ add("microsteps.changed", location, "unchanged microstep changes its active state paths")
1667
+ }
1668
+ } else if (
1669
+ microstep.exitPaths.length === 0 && microstep.entryPaths.length === 0 && sameActivePaths(before, after)
1670
+ ) {
1671
+ add("microsteps.changed", location, "changed microstep has no control-state change or reentry evidence")
1819
1672
  }
1820
- if (!sameActivePaths(before, after)) {
1821
- add("microsteps.changed", location, "unchanged microstep changes its active state paths")
1673
+ for (const transition of microstep.transitions) {
1674
+ if (
1675
+ !before.active.has(String(transition.source)) &&
1676
+ byPath.get(String(transition.source))?.type !== "choice"
1677
+ ) {
1678
+ add(
1679
+ "microsteps.activeBefore",
1680
+ location,
1681
+ `transition source "${String(transition.source)}" is inactive before the microstep`,
1682
+ String(transition.source)
1683
+ )
1684
+ }
1685
+ if (
1686
+ transition.resolvedTarget !== undefined && !after.active.has(String(transition.resolvedTarget)) &&
1687
+ microstep.transitions.length === 1
1688
+ ) {
1689
+ add(
1690
+ "microsteps.activeAfter",
1691
+ location,
1692
+ `resolved target "${String(transition.resolvedTarget)}" is inactive after the microstep`,
1693
+ String(transition.resolvedTarget)
1694
+ )
1695
+ }
1822
1696
  }
1823
- } else if (
1824
- microstep.exitPaths.length === 0 && microstep.entryPaths.length === 0 && sameActivePaths(before, after)
1825
- ) {
1826
- add("microsteps.changed", location, "changed microstep has no control-state change or reentry evidence")
1827
1697
  }
1828
- for (const transition of microstep.transitions) {
1829
- if (
1830
- !before.active.has(String(transition.source)) &&
1831
- byPath.get(String(transition.source))?.type !== "choice"
1832
- ) {
1833
- add(
1834
- "microsteps.activeBefore",
1835
- location,
1836
- `transition source "${String(transition.source)}" is inactive before the microstep`,
1837
- String(transition.source)
1838
- )
1839
- }
1840
- if (
1841
- transition.resolvedTarget !== undefined && !after.active.has(String(transition.resolvedTarget)) &&
1842
- microstep.transitions.length === 1
1843
- ) {
1844
- add(
1845
- "microsteps.activeAfter",
1846
- location,
1847
- `resolved target "${String(transition.resolvedTarget)}" is inactive after the microstep`,
1848
- String(transition.resolvedTarget)
1849
- )
1698
+ const branches = microstep.transitions.map((transition) => validateTransitionDefinition(transition, location))
1699
+ validateTransitionResolutions(microstep.transitions, branches, location)
1700
+ }
1701
+
1702
+ const validatePlanCompletion = (
1703
+ plan: PlanCompletion<M>,
1704
+ snapshot: SnapshotInspection,
1705
+ completions: ReadonlyMap<string, unknown>,
1706
+ location: VerificationLocation,
1707
+ label: string
1708
+ ): void => {
1709
+ if (!selected.has("completion")) return
1710
+ const rootPath = snapshot.paths.find((path) => byPath.get(path)?.parent === undefined)
1711
+ const hasRootDoneTransition = rootPath !== undefined &&
1712
+ definitions.some((definition) => definition.source === rootPath && definition.trigger.type === "done")
1713
+ const terminal = rootPath !== undefined && isCompletedControl(rootPath, snapshot.active) && !hasRootDoneTransition
1714
+ if (plan.done !== terminal) {
1715
+ add(
1716
+ "completion.done",
1717
+ location,
1718
+ `${label} reports done=${String(plan.done)} for terminal=${String(terminal)}`,
1719
+ rootPath
1720
+ )
1721
+ }
1722
+ if (plan.done) {
1723
+ if (rootPath === undefined || !completions.has(rootPath)) {
1724
+ add("completion.output", location, `${label} done plan has no root completion output`, rootPath)
1725
+ } else if (!sameValue(plan.output, completions.get(rootPath))) {
1726
+ add("completion.output", location, `${label} output differs from its root completion`, rootPath)
1850
1727
  }
1728
+ } else if (plan.output !== undefined) {
1729
+ add("completion.output", location, `${label} non-done plan exposes an output`, rootPath)
1851
1730
  }
1852
1731
  }
1853
- const branches = microstep.transitions.map((transition) => validateTransitionDefinition(transition, location))
1854
- validateTransitionResolutions(microstep.transitions, branches, location)
1855
- }
1856
1732
 
1857
- const validatePlanCompletion = (
1858
- plan: PlanCompletion<M>,
1859
- snapshot: SnapshotInspection,
1860
- completions: ReadonlyMap<string, unknown>,
1861
- location: VerificationLocation,
1862
- label: string
1863
- ): void => {
1864
- if (!selected.has("completion")) return
1865
- const rootPath = snapshot.paths.find((path) => byPath.get(path)?.parent === undefined)
1866
- const hasRootDoneTransition = rootPath !== undefined &&
1867
- definitions.some((definition) => definition.source === rootPath && definition.trigger.type === "done")
1868
- const terminal = rootPath !== undefined && isCompletedControl(rootPath, snapshot.active) && !hasRootDoneTransition
1869
- if (plan.done !== terminal) {
1870
- add(
1871
- "completion.done",
1872
- location,
1873
- `${label} reports done=${String(plan.done)} for terminal=${String(terminal)}`,
1874
- rootPath
1875
- )
1876
- }
1877
- if (plan.done) {
1878
- if (rootPath === undefined || !completions.has(rootPath)) {
1879
- add("completion.output", location, `${label} done plan has no root completion output`, rootPath)
1880
- } else if (!sameValue(plan.output, completions.get(rootPath))) {
1881
- add("completion.output", location, `${label} output differs from its root completion`, rootPath)
1733
+ const initialLocation: VerificationLocation = { eventIndex: undefined }
1734
+ const starting = inspectSnapshot(trace.initial.startingState, initialLocation, "initial starting state")
1735
+ if (selected.has("definitions")) {
1736
+ if (initialDefinition.selection.kind === "initial") {
1737
+ for (const path of starting.paths) {
1738
+ const node = byPath.get(path)
1739
+ if (node?.type !== "compound") continue
1740
+ const child = node.children.find((child) => starting.active.has(child))
1741
+ if (byPath.get(node.initial ?? "")?.type !== "choice" && child !== node.initial) {
1742
+ add(
1743
+ "definitions.initial",
1744
+ initialLocation,
1745
+ `initial state "${path}" must enter declared child "${node.initial}"`,
1746
+ path
1747
+ )
1748
+ }
1749
+ }
1750
+ }
1751
+ const startingRoots = starting.paths.filter((path) => byPath.get(path)?.parent === undefined)
1752
+ const routedRoot = startingRoots.length === 1 && startingRoots[0] !== initialDefinition.target
1753
+ ? initialChoiceRouteRoot()
1754
+ : undefined
1755
+ if (
1756
+ startingRoots.length !== 1 ||
1757
+ startingRoots[0] !== initialDefinition.target && startingRoots[0] !== routedRoot
1758
+ ) {
1759
+ add(
1760
+ "definitions.initial",
1761
+ initialLocation,
1762
+ `initial starting state selected roots [${startingRoots.join(", ")}] without an exact route from ` +
1763
+ `declared root "${initialDefinition.target}"`,
1764
+ startingRoots[0] ?? initialDefinition.target
1765
+ )
1882
1766
  }
1883
- } else if (plan.output !== undefined) {
1884
- add("completion.output", location, `${label} non-done plan exposes an output`, rootPath)
1885
- }
1886
- }
1887
-
1888
- const initialLocation: VerificationLocation = { eventIndex: undefined }
1889
- const starting = inspectSnapshot(trace.initial.startingState, initialLocation, "initial starting state")
1890
- if (selected.has("definitions")) {
1891
- const startingRoots = starting.paths.filter((path) => byPath.get(path)?.parent === undefined)
1892
- const routedRoot = startingRoots.length === 1 && startingRoots[0] !== initialDefinition.target
1893
- ? initialChoiceRouteRoot()
1894
- : undefined
1895
- if (
1896
- startingRoots.length !== 1 ||
1897
- startingRoots[0] !== initialDefinition.target && startingRoots[0] !== routedRoot
1898
- ) {
1899
- add(
1900
- "definitions.initial",
1901
- initialLocation,
1902
- `initial starting state selected roots [${startingRoots.join(", ")}] without an exact route from ` +
1903
- `declared root "${initialDefinition.target}"`,
1904
- startingRoots[0] ?? initialDefinition.target
1905
- )
1906
- }
1907
- }
1908
- validateSnapshotMetadata(starting, initialLocation, "initial starting state")
1909
- validateTraceConfiguration(
1910
- starting,
1911
- trace.initial.startingConfiguration as ReadonlyArray<string>,
1912
- initialLocation,
1913
- "initial starting"
1914
- )
1915
- if (selected.has("microsteps")) {
1916
- if (new Set(trace.initial.initialEntryPaths).size !== trace.initial.initialEntryPaths.length) {
1917
- add("microsteps.unique", initialLocation, "initial entry paths contain duplicates")
1918
- }
1919
- if (!samePaths(trace.initial.initialEntryPaths as ReadonlyArray<string>, starting.paths)) {
1920
- add(
1921
- "microsteps.order",
1922
- initialLocation,
1923
- "initial entry paths do not cover the starting configuration in definition order"
1924
- )
1925
1767
  }
1926
- }
1927
-
1928
- let current = starting
1929
- for (let index = 0; index < trace.initial.plan.microsteps.length; index++) {
1930
- const location: VerificationLocation = { eventIndex: undefined, microstepIndex: index }
1931
- const microstep = trace.initial.plan.microsteps[index]!
1932
- const next = inspectSnapshot(microstep.next, location, `initial microstep ${index} next state`)
1933
- validateSnapshotMetadata(next, location, `initial microstep ${index} next state`)
1934
- validateMicrostep(microstep, current, next, location)
1935
- current = next
1936
- }
1937
- const initialState = inspectSnapshot(trace.initial.plan.state, initialLocation, "initial plan state")
1938
- const initialCompletions = validateSnapshotMetadata(initialState, initialLocation, "initial plan state", true)
1939
- if (selected.has("microsteps") && !sameControl(current, initialState)) {
1940
- add("microsteps.continuity", initialLocation, "initial plan state does not continue from its final microstep")
1941
- }
1942
- validatePlanCompletion(trace.initial.plan, initialState, initialCompletions, initialLocation, "initial plan")
1943
- validateTraceConfiguration(
1944
- initialState,
1945
- trace.initial.configuration as ReadonlyArray<string>,
1946
- initialLocation,
1947
- "initial"
1948
- )
1949
- if (selected.has("microsteps") && !sameValue(trace.initial.startingState, trace.initial.plan.startingState)) {
1950
- add("microsteps.continuity", initialLocation, "initial trace starting state differs from its plan")
1951
- }
1952
- if (
1953
- selected.has("microsteps") &&
1954
- !samePaths(trace.initial.initialEntryPaths as ReadonlyArray<string>, trace.initial.plan.initialEntryPaths)
1955
- ) {
1956
- add("microsteps.continuity", initialLocation, "initial trace entry paths differ from its plan")
1957
- }
1958
-
1959
- let previous = initialState
1960
- for (let eventIndex = 0; eventIndex < trace.steps.length; eventIndex++) {
1961
- const step = trace.steps[eventIndex]!
1962
- const location: VerificationLocation = { eventIndex }
1963
- const before = inspectSnapshot(step.before, location, `event ${eventIndex} before state`)
1964
- validateSnapshotMetadata(before, location, `event ${eventIndex} before state`, true)
1768
+ validateSnapshotMetadata(starting, initialLocation, "initial starting state")
1965
1769
  validateTraceConfiguration(
1966
- before,
1967
- step.beforeConfiguration as ReadonlyArray<string>,
1968
- location,
1969
- `event ${eventIndex} before`
1770
+ starting,
1771
+ trace.initial.startingConfiguration as ReadonlyArray<string>,
1772
+ initialLocation,
1773
+ "initial starting"
1970
1774
  )
1971
1775
  if (selected.has("microsteps")) {
1972
- if (step.index !== eventIndex) {
1973
- add("microsteps.continuity", location, `trace step index ${step.index} does not equal ${eventIndex}`)
1974
- }
1975
- if (!sameValue(previous.root, before.root)) {
1976
- add("microsteps.continuity", location, `event ${eventIndex} before state does not equal the previous state`)
1776
+ if (new Set(trace.initial.initialEntryPaths).size !== trace.initial.initialEntryPaths.length) {
1777
+ add("microsteps.unique", initialLocation, "initial entry paths contain duplicates")
1977
1778
  }
1978
- if (!sameValue(step.event, trace.scenario.events[eventIndex])) {
1979
- add("microsteps.continuity", location, `event ${eventIndex} differs from its scenario event`)
1779
+ if (!samePaths(trace.initial.initialEntryPaths as ReadonlyArray<string>, starting.paths)) {
1780
+ add(
1781
+ "microsteps.order",
1782
+ initialLocation,
1783
+ "initial entry paths do not cover the starting configuration in definition order"
1784
+ )
1980
1785
  }
1981
1786
  }
1982
1787
 
1983
- current = before
1984
- for (let microstepIndex = 0; microstepIndex < step.plan.microsteps.length; microstepIndex++) {
1985
- const microstepLocation: VerificationLocation = { eventIndex, microstepIndex }
1986
- const microstep = step.plan.microsteps[microstepIndex]!
1987
- const next = inspectSnapshot(
1988
- microstep.next,
1989
- microstepLocation,
1990
- `event ${eventIndex} microstep ${microstepIndex} next state`
1991
- )
1992
- validateSnapshotMetadata(next, microstepLocation, `event ${eventIndex} microstep ${microstepIndex} next state`)
1993
- validateMicrostep(microstep, current, next, microstepLocation)
1788
+ let current = starting
1789
+ for (let index = 0; index < trace.initial.plan.microsteps.length; index++) {
1790
+ const location: VerificationLocation = { eventIndex: undefined, microstepIndex: index }
1791
+ const microstep = trace.initial.plan.microsteps[index]!
1792
+ const next = inspectSnapshot(microstep.next, location, `initial microstep ${index} next state`)
1793
+ validateSnapshotMetadata(next, location, `initial microstep ${index} next state`)
1794
+ validateMicrostep(microstep, current, next, location)
1994
1795
  current = next
1995
1796
  }
1996
-
1997
- const plannedNext = inspectSnapshot(step.plan.next, location, `event ${eventIndex} plan next state`)
1998
- const completions = validateSnapshotMetadata(plannedNext, location, `event ${eventIndex} plan next state`, true)
1999
- if (selected.has("microsteps") && !sameControl(current, plannedNext)) {
2000
- add(
2001
- "microsteps.continuity",
2002
- location,
2003
- `event ${eventIndex} plan next state does not continue its final microstep`
2004
- )
1797
+ const initialState = inspectSnapshot(trace.initial.plan.state, initialLocation, "initial plan state")
1798
+ const initialCompletions = validateSnapshotMetadata(initialState, initialLocation, "initial plan state", true)
1799
+ if (selected.has("microsteps") && !sameControl(current, initialState)) {
1800
+ add("microsteps.continuity", initialLocation, "initial plan state does not continue from its final microstep")
2005
1801
  }
2006
- validatePlanCompletion(step.plan, plannedNext, completions, location, `event ${eventIndex} plan`)
2007
-
2008
- const after = inspectSnapshot(step.after, location, `event ${eventIndex} after state`)
2009
- validateSnapshotMetadata(after, location, `event ${eventIndex} after state`, true)
1802
+ validatePlanCompletion(trace.initial.plan, initialState, initialCompletions, initialLocation, "initial plan")
2010
1803
  validateTraceConfiguration(
2011
- after,
2012
- step.afterConfiguration as ReadonlyArray<string>,
2013
- location,
2014
- `event ${eventIndex} after`
1804
+ initialState,
1805
+ trace.initial.configuration as ReadonlyArray<string>,
1806
+ initialLocation,
1807
+ "initial"
2015
1808
  )
2016
- if (selected.has("microsteps") && !sameValue(step.plan.next, step.after)) {
2017
- add("microsteps.continuity", location, `event ${eventIndex} after state differs from its plan next state`)
1809
+ if (selected.has("microsteps") && !sameValue(trace.initial.startingState, trace.initial.plan.startingState)) {
1810
+ add("microsteps.continuity", initialLocation, "initial trace starting state differs from its plan")
2018
1811
  }
2019
- previous = after
2020
- }
2021
-
2022
- const finalEventIndex = trace.steps.length === 0 ? undefined : trace.steps.length - 1
2023
- const finalLocation: VerificationLocation = { eventIndex: finalEventIndex }
2024
- const final = inspectSnapshot(trace.final, finalLocation, "trace final state")
2025
- validateSnapshotMetadata(final, finalLocation, "trace final state", true)
2026
- validateTraceConfiguration(final, trace.finalConfiguration as ReadonlyArray<string>, finalLocation, "final")
2027
- if (selected.has("microsteps") && !sameValue(previous.root, final.root)) {
2028
- add("microsteps.continuity", finalLocation, "trace final state differs from its final planned state")
2029
- }
1812
+ if (
1813
+ selected.has("microsteps") &&
1814
+ !samePaths(trace.initial.initialEntryPaths as ReadonlyArray<string>, trace.initial.plan.initialEntryPaths)
1815
+ ) {
1816
+ add("microsteps.continuity", initialLocation, "initial trace entry paths differ from its plan")
1817
+ }
1818
+
1819
+ let previous = initialState
1820
+ for (let eventIndex = 0; eventIndex < trace.steps.length; eventIndex++) {
1821
+ const step = trace.steps[eventIndex]!
1822
+ const location: VerificationLocation = { eventIndex }
1823
+ const before = inspectSnapshot(step.before, location, `event ${eventIndex} before state`)
1824
+ validateSnapshotMetadata(before, location, `event ${eventIndex} before state`, true)
1825
+ validateTraceConfiguration(
1826
+ before,
1827
+ step.beforeConfiguration as ReadonlyArray<string>,
1828
+ location,
1829
+ `event ${eventIndex} before`
1830
+ )
1831
+ if (selected.has("microsteps")) {
1832
+ if (step.index !== eventIndex) {
1833
+ add("microsteps.continuity", location, `trace step index ${step.index} does not equal ${eventIndex}`)
1834
+ }
1835
+ if (!sameValue(previous.root, before.root)) {
1836
+ add("microsteps.continuity", location, `event ${eventIndex} before state does not equal the previous state`)
1837
+ }
1838
+ if (!sameValue(step.event, trace.scenario.events[eventIndex])) {
1839
+ add("microsteps.continuity", location, `event ${eventIndex} differs from its scenario event`)
1840
+ }
1841
+ }
2030
1842
 
2031
- return violations.length === 0 ? Effect.void : Effect.fail(new VerificationError({ violations }))
2032
- }
1843
+ current = before
1844
+ for (let microstepIndex = 0; microstepIndex < step.plan.microsteps.length; microstepIndex++) {
1845
+ const microstepLocation: VerificationLocation = { eventIndex, microstepIndex }
1846
+ const microstep = step.plan.microsteps[microstepIndex]!
1847
+ const next = inspectSnapshot(
1848
+ microstep.next,
1849
+ microstepLocation,
1850
+ `event ${eventIndex} microstep ${microstepIndex} next state`
1851
+ )
1852
+ validateSnapshotMetadata(next, microstepLocation, `event ${eventIndex} microstep ${microstepIndex} next state`)
1853
+ validateMicrostep(microstep, current, next, microstepLocation)
1854
+ current = next
1855
+ }
2033
1856
 
2034
- const formatConfiguration = (paths: ReadonlyArray<string>): string => `[${paths.join(", ")}]`
2035
-
2036
- const formatMicrosteps = <M extends AnyMachine>(microsteps: ReadonlyArray<Microstep<M, any>>): Array<string> =>
2037
- microsteps.map((microstep, index) => {
2038
- const transitions = microstep.transitions.map((transition) => ({
2039
- source: transition.source,
2040
- trigger: transition.trigger,
2041
- reenter: transition.reenter,
2042
- branchIndex: transition.branchIndex,
2043
- target: transition.target,
2044
- resolvedTarget: transition.resolvedTarget
2045
- }))
2046
- return ` microstep ${index}: event=${formatValue(microstep.event)} changed=${String(microstep.changed)} ` +
2047
- `transitions=${formatValue(transitions)} exit=${formatConfiguration(microstep.exitPaths)} ` +
2048
- `entry=${formatConfiguration(microstep.entryPaths)} commands=${microstep.commands.length} ` +
2049
- `raised=${formatValue(microstep.raisedEvents)} emitted=${formatValue(microstep.emittedEvents)} ` +
2050
- `next=${formatValue(microstep.next)}`
2051
- })
1857
+ const plannedNext = inspectSnapshot(step.plan.next, location, `event ${eventIndex} plan next state`)
1858
+ const completions = validateSnapshotMetadata(plannedNext, location, `event ${eventIndex} plan next state`, true)
1859
+ if (selected.has("microsteps") && !sameControl(current, plannedNext)) {
1860
+ add(
1861
+ "microsteps.continuity",
1862
+ location,
1863
+ `event ${eventIndex} plan next state does not continue its final microstep`
1864
+ )
1865
+ }
1866
+ validatePlanCompletion(step.plan, plannedNext, completions, location, `event ${eventIndex} plan`)
2052
1867
 
2053
- const formatInitial = <M extends AnyMachine>(initial: InitialTrace<M>): Array<string> => [
2054
- `initial: startingConfiguration=${formatConfiguration(initial.startingConfiguration)} ` +
2055
- `startingState=${formatValue(initial.startingState)} initialEntry=${
2056
- formatConfiguration(initial.initialEntryPaths)
2057
- } ` +
2058
- `configuration=${formatConfiguration(initial.configuration)} state=${formatValue(initial.plan.state)} ` +
2059
- `done=${String(initial.plan.done)} output=${formatValue(initial.plan.output)} ` +
2060
- `commands=${initial.plan.commands.length} emitted=${formatValue(initial.plan.emittedEvents)}`,
2061
- ...formatMicrosteps(initial.plan.microsteps)
2062
- ]
2063
-
2064
- const formatStep = <M extends AnyMachine>(step: TraceStep<M>): Array<string> => [
2065
- `step ${step.index}: event=${formatValue(step.event)} before=${formatConfiguration(step.beforeConfiguration)} ` +
2066
- `after=${formatConfiguration(step.afterConfiguration)} state=${formatValue(step.after)} ` +
2067
- `done=${String(step.plan.done)} output=${formatValue(step.plan.output)} ` +
2068
- `commands=${step.plan.commands.length} emitted=${formatValue(step.plan.emittedEvents)}`,
2069
- ...formatMicrosteps(step.plan.microsteps)
2070
- ]
2071
-
2072
- const isRunFailure = <M extends AnyMachine, Cause>(
2073
- trace: Trace<M> | RunFailure<Cause, M>
2074
- ): trace is RunFailure<Cause, M> => "_tag" in trace && trace._tag === "MachineTestRunFailure"
2075
-
2076
- export const formatTrace = <M extends AnyMachine, Cause>(trace: Trace<M> | RunFailure<Cause, M>): string => {
2077
- const lines = [`scenario: ${formatValue(trace.scenario)}`]
2078
- if (isRunFailure(trace)) {
2079
- if (trace.initial !== undefined) {
2080
- lines.push(...formatInitial(trace.initial))
2081
- for (const step of trace.steps) {
2082
- lines.push(...formatStep(step))
1868
+ const after = inspectSnapshot(step.after, location, `event ${eventIndex} after state`)
1869
+ validateSnapshotMetadata(after, location, `event ${eventIndex} after state`, true)
1870
+ validateTraceConfiguration(
1871
+ after,
1872
+ step.afterConfiguration as ReadonlyArray<string>,
1873
+ location,
1874
+ `event ${eventIndex} after`
1875
+ )
1876
+ if (selected.has("microsteps") && !sameValue(step.plan.next, step.after)) {
1877
+ add("microsteps.continuity", location, `event ${eventIndex} after state differs from its plan next state`)
2083
1878
  }
1879
+ previous = after
2084
1880
  }
2085
- lines.push(
2086
- `failure: phase=${trace.phase} eventIndex=${formatValue(trace.eventIndex)} ` +
2087
- `event=${formatValue(trace.event)} cause=${formatValue(trace.cause)}`
2088
- )
2089
- return lines.join("\n")
2090
- }
2091
- lines.push(...formatInitial(trace.initial))
2092
- for (const step of trace.steps) {
2093
- lines.push(...formatStep(step))
2094
- }
2095
- lines.push(`final: configuration=${formatConfiguration(trace.finalConfiguration)} state=${formatValue(trace.final)}`)
2096
- return lines.join("\n")
2097
- }
1881
+
1882
+ const finalEventIndex = trace.steps.length === 0 ? undefined : trace.steps.length - 1
1883
+ const finalLocation: VerificationLocation = { eventIndex: finalEventIndex }
1884
+ const final = inspectSnapshot(trace.final, finalLocation, "trace final state")
1885
+ validateSnapshotMetadata(final, finalLocation, "trace final state", true)
1886
+ validateTraceConfiguration(final, trace.finalConfiguration as ReadonlyArray<string>, finalLocation, "final")
1887
+ if (selected.has("microsteps") && !sameValue(previous.root, final.root)) {
1888
+ add("microsteps.continuity", finalLocation, "trace final state differs from its final planned state")
1889
+ }
1890
+
1891
+ return violations.length === 0 ? Effect.void : Effect.fail(new VerificationError({ violations }))
1892
+ })