@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.
- package/README.md +145 -140
- package/dist/Machine.d.ts +428 -254
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +91 -111
- package/dist/Machine.js.map +1 -1
- package/dist/internal/machine/atom.d.ts +3 -4
- package/dist/internal/machine/atom.d.ts.map +1 -1
- package/dist/internal/machine/atom.js +0 -2
- package/dist/internal/machine/atom.js.map +1 -1
- package/dist/internal/machine/childRegistry.d.ts +52 -0
- package/dist/internal/machine/childRegistry.d.ts.map +1 -0
- package/dist/internal/machine/childRegistry.js +74 -0
- package/dist/internal/machine/childRegistry.js.map +1 -0
- package/dist/internal/machine/cluster.d.ts +2 -3
- package/dist/internal/machine/cluster.d.ts.map +1 -1
- package/dist/internal/machine/cluster.js.map +1 -1
- package/dist/internal/machine/configuration.d.ts.map +1 -1
- package/dist/internal/machine/configuration.js +14 -4
- package/dist/internal/machine/configuration.js.map +1 -1
- package/dist/internal/machine/executionPlan.d.ts +1 -0
- package/dist/internal/machine/executionPlan.d.ts.map +1 -1
- package/dist/internal/machine/executionPlan.js +97 -53
- package/dist/internal/machine/executionPlan.js.map +1 -1
- package/dist/internal/machine/invocation.d.ts.map +1 -1
- package/dist/internal/machine/invocation.js +4 -2
- package/dist/internal/machine/invocation.js.map +1 -1
- package/dist/internal/machine/invocationDefinition.d.ts +35 -0
- package/dist/internal/machine/invocationDefinition.d.ts.map +1 -0
- package/dist/internal/machine/invocationDefinition.js +21 -0
- package/dist/internal/machine/invocationDefinition.js.map +1 -0
- package/dist/internal/machine/invocationEvent.d.ts +0 -2
- package/dist/internal/machine/invocationEvent.d.ts.map +1 -1
- package/dist/internal/machine/invocationEvent.js +0 -8
- package/dist/internal/machine/invocationEvent.js.map +1 -1
- package/dist/internal/machine/machine.d.ts +21 -43
- package/dist/internal/machine/machine.d.ts.map +1 -1
- package/dist/internal/machine/machine.js +98 -374
- package/dist/internal/machine/machine.js.map +1 -1
- package/dist/internal/machine/planner.d.ts +13 -19
- package/dist/internal/machine/planner.d.ts.map +1 -1
- package/dist/internal/machine/planner.js +63 -35
- package/dist/internal/machine/planner.js.map +1 -1
- package/dist/internal/machine/process.d.ts +6 -8
- package/dist/internal/machine/process.d.ts.map +1 -1
- package/dist/internal/machine/process.js +1 -0
- package/dist/internal/machine/process.js.map +1 -1
- package/dist/internal/machine/protocol.d.ts +2 -2
- package/dist/internal/machine/protocol.d.ts.map +1 -1
- package/dist/internal/machine/protocol.js +4 -18
- package/dist/internal/machine/protocol.js.map +1 -1
- package/dist/internal/machine/readiness.d.ts +1 -1
- package/dist/internal/machine/readiness.d.ts.map +1 -1
- package/dist/internal/machine/requirements.d.ts +4 -0
- package/dist/internal/machine/requirements.d.ts.map +1 -0
- package/dist/internal/machine/requirements.js +2 -0
- package/dist/internal/machine/requirements.js.map +1 -0
- package/dist/internal/machine/runtime.d.ts +4 -58
- package/dist/internal/machine/runtime.d.ts.map +1 -1
- package/dist/internal/machine/runtime.js +1 -69
- package/dist/internal/machine/runtime.js.map +1 -1
- package/dist/internal/machine/serialization.d.ts.map +1 -1
- package/dist/internal/machine/serialization.js +16 -0
- package/dist/internal/machine/serialization.js.map +1 -1
- package/dist/internal/machine/stateDefinition.d.ts +3 -1
- package/dist/internal/machine/stateDefinition.d.ts.map +1 -1
- package/dist/internal/machine/stateDefinition.js +36 -2
- package/dist/internal/machine/stateDefinition.js.map +1 -1
- package/dist/internal/machine/targetBuilder.d.ts +16 -0
- package/dist/internal/machine/targetBuilder.d.ts.map +1 -0
- package/dist/internal/machine/targetBuilder.js +333 -0
- package/dist/internal/machine/targetBuilder.js.map +1 -0
- package/dist/internal/machine/topology.js +3 -3
- package/dist/internal/machine/topology.js.map +1 -1
- package/dist/internal/machine/transition.d.ts +16 -0
- package/dist/internal/machine/transition.d.ts.map +1 -0
- package/dist/internal/machine/transition.js +2 -0
- package/dist/internal/machine/transition.js.map +1 -0
- package/dist/internal/testing/machine/finiteModel.d.ts.map +1 -1
- package/dist/internal/testing/machine/finiteModel.js +10 -12
- package/dist/internal/testing/machine/finiteModel.js.map +1 -1
- package/dist/internal/testing/machine/format.d.ts +7 -0
- package/dist/internal/testing/machine/format.d.ts.map +1 -0
- package/dist/internal/testing/machine/format.js +104 -0
- package/dist/internal/testing/machine/format.js.map +1 -0
- package/dist/internal/testing/machine/probe.d.ts +1 -1
- package/dist/internal/testing/machine/probe.d.ts.map +1 -1
- package/dist/internal/testing/machine/probe.js +1 -1
- package/dist/internal/testing/machine/probe.js.map +1 -1
- package/dist/internal/testing/machine/referenceModel.d.ts.map +1 -1
- package/dist/internal/testing/machine/referenceModel.js +15 -19
- package/dist/internal/testing/machine/referenceModel.js.map +1 -1
- package/dist/internal/testing/machine/trace.d.ts +1 -1
- package/dist/internal/testing/machine/trace.d.ts.map +1 -1
- package/dist/internal/testing/machine/trace.js +3 -3
- package/dist/internal/testing/machine/trace.js.map +1 -1
- package/dist/internal/testing/machine/value.d.ts +8 -0
- package/dist/internal/testing/machine/value.d.ts.map +1 -0
- package/dist/internal/testing/machine/value.js +82 -0
- package/dist/internal/testing/machine/value.js.map +1 -0
- package/dist/internal/testing/machine/verification.d.ts +2 -13
- package/dist/internal/testing/machine/verification.d.ts.map +1 -1
- package/dist/internal/testing/machine/verification.js +21 -113
- package/dist/internal/testing/machine/verification.js.map +1 -1
- package/dist/testing/MachineTest.d.ts +89 -104
- package/dist/testing/MachineTest.d.ts.map +1 -1
- package/dist/testing/MachineTest.js +70 -84
- package/dist/testing/MachineTest.js.map +1 -1
- package/dist/unstable/cluster/ClusterMachine.d.ts +10 -13
- package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -1
- package/dist/unstable/cluster/ClusterMachine.js +5 -8
- package/dist/unstable/cluster/ClusterMachine.js.map +1 -1
- package/dist/unstable/reactivity/AtomMachine.d.ts +26 -61
- package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
- package/dist/unstable/reactivity/AtomMachine.js +17 -26
- package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
- package/docs/agent-guide.md +45 -45
- package/docs/effect-atom-react.md +35 -63
- package/docs/machine-review.md +7 -7
- package/docs/root-api.md +214 -0
- package/package.json +1 -1
- package/src/Machine.ts +778 -437
- package/src/internal/machine/atom.ts +3 -13
- package/src/internal/machine/childRegistry.ts +141 -0
- package/src/internal/machine/cluster.ts +5 -14
- package/src/internal/machine/configuration.ts +20 -4
- package/src/internal/machine/executionPlan.ts +109 -52
- package/src/internal/machine/invocation.ts +10 -6
- package/src/internal/machine/invocationDefinition.ts +52 -0
- package/src/internal/machine/invocationEvent.ts +0 -8
- package/src/internal/machine/machine.ts +154 -679
- package/src/internal/machine/planner.ts +74 -62
- package/src/internal/machine/process.ts +14 -21
- package/src/internal/machine/protocol.ts +9 -32
- package/src/internal/machine/readiness.ts +1 -0
- package/src/internal/machine/requirements.ts +11 -0
- package/src/internal/machine/runtime.ts +17 -186
- package/src/internal/machine/serialization.ts +22 -0
- package/src/internal/machine/stateDefinition.ts +36 -3
- package/src/internal/machine/targetBuilder.ts +525 -0
- package/src/internal/machine/topology.ts +3 -3
- package/src/internal/machine/transition.ts +26 -0
- package/src/internal/testing/machine/finiteModel.ts +22 -23
- package/src/internal/testing/machine/format.ts +120 -0
- package/src/internal/testing/machine/probe.ts +4 -4
- package/src/internal/testing/machine/referenceModel.ts +25 -17
- package/src/internal/testing/machine/trace.ts +4 -4
- package/src/internal/testing/machine/value.ts +77 -0
- package/src/internal/testing/machine/verification.ts +904 -1109
- package/src/testing/MachineTest.ts +103 -114
- package/src/unstable/cluster/ClusterMachine.ts +10 -20
- 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
|
|
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
|
|
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
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
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
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
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
|
-
|
|
1064
|
-
paths.push(path)
|
|
1065
|
-
values.set(path, current.value)
|
|
872
|
+
}
|
|
1066
873
|
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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 (
|
|
1073
|
-
|
|
1074
|
-
|
|
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
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
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
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
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
|
-
|
|
1109
|
-
if (
|
|
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
|
-
|
|
1119
|
-
|
|
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 (
|
|
1122
|
-
add(
|
|
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
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
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.
|
|
940
|
+
add("configuration.path", location, `${label} activates ${node.type} pseudo-state "${path}"`, path)
|
|
1131
941
|
}
|
|
1132
942
|
return
|
|
1133
943
|
}
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
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.
|
|
954
|
+
"configuration.compound",
|
|
1143
955
|
location,
|
|
1144
|
-
`${label}
|
|
1145
|
-
|
|
956
|
+
`${label} compound state "${path}" must have exactly one child`,
|
|
957
|
+
path
|
|
1146
958
|
)
|
|
1147
959
|
}
|
|
1148
960
|
} else {
|
|
1149
|
-
visit(current.
|
|
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
|
-
|
|
1153
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1172
|
-
|
|
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
|
-
|
|
1181
|
-
|
|
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
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
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
|
-
"
|
|
1030
|
+
"configuration.trace",
|
|
1230
1031
|
location,
|
|
1231
|
-
`${label}
|
|
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
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
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
|
-
|
|
1254
|
-
|
|
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
|
|
1265
|
-
|
|
1266
|
-
if (!inOwnerSubtree && !inOwnerAncestry) {
|
|
1058
|
+
const entry = unknownEntry
|
|
1059
|
+
if (entry.mode !== historyNode.history) {
|
|
1267
1060
|
add(
|
|
1268
|
-
"history.
|
|
1061
|
+
"history.mode",
|
|
1269
1062
|
location,
|
|
1270
|
-
`${label} history record "${historyPath}"
|
|
1271
|
-
|
|
1063
|
+
`${label} history record "${historyPath}" has mode "${
|
|
1064
|
+
String(entry.mode)
|
|
1065
|
+
}", expected "${historyNode.history}"`,
|
|
1066
|
+
historyPath
|
|
1272
1067
|
)
|
|
1273
1068
|
}
|
|
1274
|
-
|
|
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.
|
|
1071
|
+
"history.record",
|
|
1287
1072
|
location,
|
|
1288
|
-
`${label} history
|
|
1289
|
-
|
|
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.
|
|
1080
|
+
"history.record",
|
|
1298
1081
|
location,
|
|
1299
|
-
`${label}
|
|
1300
|
-
|
|
1082
|
+
`${label} history record "${historyPath}" must contain a values record`,
|
|
1083
|
+
historyPath
|
|
1301
1084
|
)
|
|
1085
|
+
continue
|
|
1302
1086
|
}
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
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
|
-
|
|
1310
|
-
|
|
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
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
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
|
-
|
|
1172
|
+
"history.path",
|
|
1346
1173
|
location,
|
|
1347
|
-
`${label} history record "${historyPath}"
|
|
1348
|
-
|
|
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
|
-
}
|
|
1354
|
-
|
|
1355
|
-
|
|
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}"
|
|
1360
|
-
|
|
1189
|
+
`${label} history record "${historyPath}" must remember one child of compound state "${path}"`,
|
|
1190
|
+
path
|
|
1361
1191
|
)
|
|
1362
1192
|
} else if (recurse) {
|
|
1363
|
-
validateRememberedControl(
|
|
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
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
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
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
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
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
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
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
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
|
-
|
|
1468
|
-
|
|
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
|
-
|
|
1471
|
-
|
|
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
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
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
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
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
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
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
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
transition.
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
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
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
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
|
-
|
|
1595
|
-
const targetNode = byPath.get(target)
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
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
|
-
|
|
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
|
|
1636
|
-
|
|
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
|
-
|
|
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
|
|
1680
|
-
|
|
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
|
-
|
|
1686
|
-
|
|
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
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
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
|
|
1720
|
-
|
|
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
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
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 (
|
|
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
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
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
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
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
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
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
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
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 (
|
|
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
|
-
`
|
|
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
|
-
`
|
|
1656
|
+
`reentered common state "${path}" must have both exit and entry lifecycle`,
|
|
1793
1657
|
path
|
|
1794
1658
|
)
|
|
1795
1659
|
}
|
|
1796
1660
|
}
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
"microsteps.
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
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
|
-
|
|
1821
|
-
|
|
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
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
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
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
)
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
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
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1770
|
+
starting,
|
|
1771
|
+
trace.initial.startingConfiguration as ReadonlyArray<string>,
|
|
1772
|
+
initialLocation,
|
|
1773
|
+
"initial starting"
|
|
1970
1774
|
)
|
|
1971
1775
|
if (selected.has("microsteps")) {
|
|
1972
|
-
if (
|
|
1973
|
-
add("microsteps.
|
|
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 (!
|
|
1979
|
-
add(
|
|
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 =
|
|
1984
|
-
for (let
|
|
1985
|
-
const
|
|
1986
|
-
const microstep =
|
|
1987
|
-
const next = inspectSnapshot(
|
|
1988
|
-
|
|
1989
|
-
|
|
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
|
|
1998
|
-
|
|
1999
|
-
|
|
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(
|
|
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
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
1804
|
+
initialState,
|
|
1805
|
+
trace.initial.configuration as ReadonlyArray<string>,
|
|
1806
|
+
initialLocation,
|
|
1807
|
+
"initial"
|
|
2015
1808
|
)
|
|
2016
|
-
if (selected.has("microsteps") && !sameValue(
|
|
2017
|
-
add("microsteps.continuity",
|
|
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
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
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
|
-
|
|
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
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
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
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
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
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
)
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
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
|
+
})
|