@typeonce/effect-machine 0.2.0 → 0.4.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 +345 -42
- package/dist/Machine.d.ts +1273 -347
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +161 -426
- package/dist/Machine.js.map +1 -1
- package/dist/internal/machine/activities.d.ts +57 -0
- package/dist/internal/machine/activities.d.ts.map +1 -0
- package/dist/internal/machine/activities.js +49 -0
- package/dist/internal/machine/activities.js.map +1 -0
- package/dist/internal/machine/atom.d.ts +69 -0
- package/dist/internal/machine/atom.d.ts.map +1 -0
- package/dist/{AtomMachine.js → internal/machine/atom.js} +19 -94
- package/dist/internal/machine/atom.js.map +1 -0
- package/dist/internal/machine/cluster.d.ts +63 -0
- package/dist/internal/machine/cluster.d.ts.map +1 -0
- package/dist/{ClusterMachine.js → internal/machine/cluster.js} +15 -119
- package/dist/internal/machine/cluster.js.map +1 -0
- package/dist/internal/machine/command.d.ts +16 -0
- package/dist/internal/machine/command.d.ts.map +1 -0
- package/dist/internal/machine/command.js +45 -0
- package/dist/internal/machine/command.js.map +1 -0
- package/dist/internal/machine/commandRuntime.d.ts +13 -0
- package/dist/internal/machine/commandRuntime.d.ts.map +1 -0
- package/dist/internal/machine/commandRuntime.js +16 -0
- package/dist/internal/machine/commandRuntime.js.map +1 -0
- package/dist/internal/{machineModel.d.ts → machine/configuration.d.ts} +32 -38
- package/dist/internal/machine/configuration.d.ts.map +1 -0
- package/dist/internal/machine/configuration.js +965 -0
- package/dist/internal/machine/configuration.js.map +1 -0
- package/dist/internal/{machineErrors.d.ts → machine/errors.d.ts} +3 -3
- package/dist/internal/machine/errors.d.ts.map +1 -0
- package/dist/internal/{machineErrors.js → machine/errors.js} +1 -1
- package/dist/internal/machine/errors.js.map +1 -0
- package/dist/internal/machine/executionPlan.d.ts +49 -0
- package/dist/internal/machine/executionPlan.d.ts.map +1 -0
- package/dist/internal/machine/executionPlan.js +667 -0
- package/dist/internal/machine/executionPlan.js.map +1 -0
- package/dist/internal/machine/invocation.d.ts +23 -0
- package/dist/internal/machine/invocation.d.ts.map +1 -0
- package/dist/internal/machine/invocation.js +77 -0
- package/dist/internal/machine/invocation.js.map +1 -0
- package/dist/internal/machine/machine.d.ts +183 -0
- package/dist/internal/machine/machine.d.ts.map +1 -0
- package/dist/internal/machine/machine.js +552 -0
- package/dist/internal/machine/machine.js.map +1 -0
- package/dist/internal/machine/planner.d.ts +182 -0
- package/dist/internal/machine/planner.d.ts.map +1 -0
- package/dist/internal/machine/planner.js +1082 -0
- package/dist/internal/machine/planner.js.map +1 -0
- package/dist/internal/{machineProcess.d.ts → machine/process.d.ts} +10 -4
- package/dist/internal/machine/process.d.ts.map +1 -0
- package/dist/internal/machine/process.js +446 -0
- package/dist/internal/machine/process.js.map +1 -0
- package/dist/internal/machine/protocol.d.ts +34 -0
- package/dist/internal/machine/protocol.d.ts.map +1 -0
- package/dist/internal/machine/protocol.js +182 -0
- package/dist/internal/machine/protocol.js.map +1 -0
- package/dist/internal/machine/readiness.d.ts +4 -0
- package/dist/internal/machine/readiness.d.ts.map +1 -0
- package/dist/internal/machine/readiness.js +2 -0
- package/dist/internal/machine/readiness.js.map +1 -0
- package/dist/internal/{machineRuntime.d.ts → machine/runtime.d.ts} +104 -2
- package/dist/internal/machine/runtime.d.ts.map +1 -0
- package/dist/internal/machine/runtime.js +1381 -0
- package/dist/internal/machine/runtime.js.map +1 -0
- package/dist/internal/machine/serialization.d.ts +14 -0
- package/dist/internal/machine/serialization.d.ts.map +1 -0
- package/dist/internal/machine/serialization.js +338 -0
- package/dist/internal/machine/serialization.js.map +1 -0
- package/dist/internal/machine/stateDefinition.d.ts +15 -0
- package/dist/internal/machine/stateDefinition.d.ts.map +1 -0
- package/dist/internal/machine/stateDefinition.js +210 -0
- package/dist/internal/machine/stateDefinition.js.map +1 -0
- package/dist/internal/machine/symbols.d.ts +3 -0
- package/dist/internal/machine/symbols.d.ts.map +1 -0
- package/dist/internal/machine/symbols.js +3 -0
- package/dist/internal/machine/symbols.js.map +1 -0
- package/dist/internal/machine/topology.d.ts +98 -0
- package/dist/internal/machine/topology.d.ts.map +1 -0
- package/dist/internal/machine/topology.js +347 -0
- package/dist/internal/machine/topology.js.map +1 -0
- package/dist/internal/testing/machine/arbitrary.d.ts +36 -0
- package/dist/internal/testing/machine/arbitrary.d.ts.map +1 -0
- package/dist/internal/testing/machine/arbitrary.js +68 -0
- package/dist/internal/testing/machine/arbitrary.js.map +1 -0
- package/dist/internal/testing/machine/finiteModel.d.ts +322 -0
- package/dist/internal/testing/machine/finiteModel.d.ts.map +1 -0
- package/dist/internal/testing/machine/finiteModel.js +976 -0
- package/dist/internal/testing/machine/finiteModel.js.map +1 -0
- package/dist/internal/testing/machine/referenceModel.d.ts +203 -0
- package/dist/internal/testing/machine/referenceModel.d.ts.map +1 -0
- package/dist/internal/testing/machine/referenceModel.js +1012 -0
- package/dist/internal/testing/machine/referenceModel.js.map +1 -0
- package/dist/internal/testing/machine/runtime.d.ts +328 -0
- package/dist/internal/testing/machine/runtime.d.ts.map +1 -0
- package/dist/internal/testing/machine/runtime.js +437 -0
- package/dist/internal/testing/machine/runtime.js.map +1 -0
- package/dist/internal/testing/machine/verification.d.ts +33 -0
- package/dist/internal/testing/machine/verification.d.ts.map +1 -0
- package/dist/internal/testing/machine/verification.js +1446 -0
- package/dist/internal/testing/machine/verification.js.map +1 -0
- package/dist/testing/MachineTest.d.ts +606 -0
- package/dist/testing/MachineTest.d.ts.map +1 -0
- package/dist/testing/MachineTest.js +102 -0
- package/dist/testing/MachineTest.js.map +1 -0
- package/dist/testing/index.d.ts +7 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +7 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/{ClusterMachine.d.ts → unstable/cluster/ClusterMachine.d.ts} +22 -30
- package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -0
- package/dist/unstable/cluster/ClusterMachine.js +126 -0
- package/dist/unstable/cluster/ClusterMachine.js.map +1 -0
- package/dist/{cluster.js → unstable/cluster/index.d.ts} +1 -1
- package/dist/unstable/cluster/index.d.ts.map +1 -0
- package/dist/{cluster.d.ts → unstable/cluster/index.js} +1 -1
- package/dist/unstable/cluster/index.js.map +1 -0
- package/dist/{AtomMachine.d.ts → unstable/reactivity/AtomMachine.d.ts} +40 -23
- package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -0
- package/dist/unstable/reactivity/AtomMachine.js +113 -0
- package/dist/unstable/reactivity/AtomMachine.js.map +1 -0
- package/dist/{reactivity.js → unstable/reactivity/index.d.ts} +1 -1
- package/dist/unstable/reactivity/index.d.ts.map +1 -0
- package/dist/{reactivity.d.ts → unstable/reactivity/index.js} +1 -1
- package/dist/unstable/reactivity/index.js.map +1 -0
- package/docs/agent-guide.md +271 -38
- package/package.json +23 -13
- package/dist/AtomMachine.d.ts.map +0 -1
- package/dist/AtomMachine.js.map +0 -1
- package/dist/ClusterMachine.d.ts.map +0 -1
- package/dist/ClusterMachine.js.map +0 -1
- package/dist/cluster.d.ts.map +0 -1
- package/dist/cluster.js.map +0 -1
- package/dist/internal/machineErrors.d.ts.map +0 -1
- package/dist/internal/machineErrors.js.map +0 -1
- package/dist/internal/machineModel.d.ts.map +0 -1
- package/dist/internal/machineModel.js +0 -766
- package/dist/internal/machineModel.js.map +0 -1
- package/dist/internal/machinePlanner.d.ts +0 -74
- package/dist/internal/machinePlanner.d.ts.map +0 -1
- package/dist/internal/machinePlanner.js +0 -621
- package/dist/internal/machinePlanner.js.map +0 -1
- package/dist/internal/machineProcess.d.ts.map +0 -1
- package/dist/internal/machineProcess.js +0 -181
- package/dist/internal/machineProcess.js.map +0 -1
- package/dist/internal/machineRuntime.d.ts.map +0 -1
- package/dist/internal/machineRuntime.js +0 -365
- package/dist/internal/machineRuntime.js.map +0 -1
- package/dist/reactivity.d.ts.map +0 -1
- package/dist/reactivity.js.map +0 -1
|
@@ -0,0 +1,1446 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Property-based scenario generation and planner trace utilities.
|
|
3
|
+
*
|
|
4
|
+
* @since 4.0.0
|
|
5
|
+
*/
|
|
6
|
+
import * as Data from "effect/Data";
|
|
7
|
+
import * as Effect from "effect/Effect";
|
|
8
|
+
import * as Graph from "effect/Graph";
|
|
9
|
+
import * as Schema from "effect/Schema";
|
|
10
|
+
import * as SchemaAST from "effect/SchemaAST";
|
|
11
|
+
import { FastCheck } from "effect/testing";
|
|
12
|
+
import * as Machine from "../../../Machine.js";
|
|
13
|
+
import { toArbitraryWithReport } from "./arbitrary.js";
|
|
14
|
+
import * as ReferenceModel from "./referenceModel.js";
|
|
15
|
+
export { advanceCommand, checkpointCommand, formatRuntimeTranscript, runRuntimeCommands, RuntimeCommandFailure, runtimeCommands, RuntimeObservationError, RuntimeSynchronization, sendCommand, stopCommand } from "./runtime.js";
|
|
16
|
+
export { compileModel, finiteModels } from "./finiteModel.js";
|
|
17
|
+
export { ModelVerificationError } from "./referenceModel.js";
|
|
18
|
+
export const interpretModel = ReferenceModel.interpretModel;
|
|
19
|
+
const validateLength = (name, value) => {
|
|
20
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
21
|
+
throw new Error(`MachineTest.scenarios expected ${name} to be a non-negative safe integer`);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
export const scenarios = (machine, options = {}) => {
|
|
25
|
+
const minEvents = options.minEvents ?? 0;
|
|
26
|
+
const maxEvents = options.maxEvents ?? 50;
|
|
27
|
+
if (options.eventsArbitrary === undefined) {
|
|
28
|
+
validateLength("minEvents", minEvents);
|
|
29
|
+
validateLength("maxEvents", maxEvents);
|
|
30
|
+
if (minEvents > maxEvents) {
|
|
31
|
+
throw new Error("MachineTest.scenarios expected minEvents to be less than or equal to maxEvents");
|
|
32
|
+
}
|
|
33
|
+
if (machine.events.length === 0 && minEvents > 0) {
|
|
34
|
+
throw new Error("MachineTest.scenarios cannot generate a non-empty event sequence for a machine without public events");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const diagnostics = [];
|
|
38
|
+
const eventArbitraries = options.eventsArbitrary === undefined
|
|
39
|
+
? machine.events.map((schema, index) => {
|
|
40
|
+
const derived = toArbitraryWithReport(schema);
|
|
41
|
+
diagnostics.push({
|
|
42
|
+
boundary: "event",
|
|
43
|
+
index,
|
|
44
|
+
report: derived.report
|
|
45
|
+
});
|
|
46
|
+
return derived.value;
|
|
47
|
+
})
|
|
48
|
+
: [];
|
|
49
|
+
const eventsArbitrary = options.eventsArbitrary ?? (eventArbitraries.length === 0
|
|
50
|
+
? FastCheck.constant([])
|
|
51
|
+
: FastCheck.array(FastCheck.oneof(...eventArbitraries), { minLength: minEvents, maxLength: maxEvents }));
|
|
52
|
+
if (machine.input === undefined || machine.input === Schema.Void) {
|
|
53
|
+
if (options.inputArbitrary !== undefined) {
|
|
54
|
+
throw new Error("MachineTest.scenarios cannot override input for a machine without an input schema");
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
arbitrary: eventsArbitrary.map((events) => ({ events })),
|
|
58
|
+
diagnostics: {
|
|
59
|
+
input: "none",
|
|
60
|
+
events: options.eventsArbitrary !== undefined ? "override" : eventArbitraries.length === 0 ? "empty" : "schema",
|
|
61
|
+
schemas: diagnostics
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
let inputArbitrary;
|
|
66
|
+
if (options.inputArbitrary !== undefined) {
|
|
67
|
+
inputArbitrary = options.inputArbitrary;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
const derived = toArbitraryWithReport(machine.input);
|
|
71
|
+
diagnostics.unshift({
|
|
72
|
+
boundary: "input",
|
|
73
|
+
index: undefined,
|
|
74
|
+
report: derived.report
|
|
75
|
+
});
|
|
76
|
+
inputArbitrary = derived.value;
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
arbitrary: FastCheck.tuple(inputArbitrary, eventsArbitrary).map(([input, events]) => ({
|
|
80
|
+
input,
|
|
81
|
+
events
|
|
82
|
+
})),
|
|
83
|
+
diagnostics: {
|
|
84
|
+
input: options.inputArbitrary !== undefined ? "override" : "schema",
|
|
85
|
+
events: options.eventsArbitrary !== undefined ? "override" : eventArbitraries.length === 0 ? "empty" : "schema",
|
|
86
|
+
schemas: diagnostics
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
export const verifyModel = (model, actualTrace) => ReferenceModel.verifyModelTrace(model, actualTrace);
|
|
91
|
+
const rawConfigurationPaths = (machine, snapshot) => {
|
|
92
|
+
const active = new Set();
|
|
93
|
+
const visit = (current) => {
|
|
94
|
+
if (typeof current !== "object" || current === null)
|
|
95
|
+
return;
|
|
96
|
+
const value = current;
|
|
97
|
+
if (typeof value.path === "string")
|
|
98
|
+
active.add(value.path);
|
|
99
|
+
if (value.state !== undefined)
|
|
100
|
+
visit(value.state);
|
|
101
|
+
if (typeof value.states === "object" && value.states !== null) {
|
|
102
|
+
for (const child of Object.values(value.states))
|
|
103
|
+
visit(child);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
visit(snapshot);
|
|
107
|
+
return Machine.stateNodes(machine)
|
|
108
|
+
.filter((node) => node.type !== "history" && node.type !== "choice" && active.has(node.path))
|
|
109
|
+
.map((node) => node.path);
|
|
110
|
+
};
|
|
111
|
+
export const run = Effect.fnUntraced(function* (machine, scenario) {
|
|
112
|
+
const initialEffect = (machine.input === undefined || machine.input === Schema.Void
|
|
113
|
+
? Machine.planInitial(machine)
|
|
114
|
+
: Machine.planInitial(machine, scenario.input));
|
|
115
|
+
const initial = yield* initialEffect.pipe(Effect.mapError((cause) => ({
|
|
116
|
+
_tag: "MachineTestRunFailure",
|
|
117
|
+
scenario,
|
|
118
|
+
phase: "initial",
|
|
119
|
+
eventIndex: undefined,
|
|
120
|
+
event: undefined,
|
|
121
|
+
initial: undefined,
|
|
122
|
+
steps: [],
|
|
123
|
+
cause
|
|
124
|
+
})));
|
|
125
|
+
const initialConfiguration = rawConfigurationPaths(machine, initial.state);
|
|
126
|
+
const initialTrace = {
|
|
127
|
+
plan: initial,
|
|
128
|
+
startingState: initial.startingState,
|
|
129
|
+
startingConfiguration: rawConfigurationPaths(machine, initial.startingState),
|
|
130
|
+
initialEntryPaths: initial.initialEntryPaths,
|
|
131
|
+
configuration: initialConfiguration
|
|
132
|
+
};
|
|
133
|
+
const steps = [];
|
|
134
|
+
let current = initial.state;
|
|
135
|
+
for (let index = 0; index < scenario.events.length; index++) {
|
|
136
|
+
const event = scenario.events[index];
|
|
137
|
+
const before = current;
|
|
138
|
+
const beforeConfiguration = rawConfigurationPaths(machine, before);
|
|
139
|
+
const plan = yield* Machine.plan(machine, before, event).pipe(Effect.mapError((cause) => ({
|
|
140
|
+
_tag: "MachineTestRunFailure",
|
|
141
|
+
scenario,
|
|
142
|
+
phase: "event",
|
|
143
|
+
eventIndex: index,
|
|
144
|
+
event,
|
|
145
|
+
initial: initialTrace,
|
|
146
|
+
steps: steps.slice(),
|
|
147
|
+
cause
|
|
148
|
+
})));
|
|
149
|
+
current = plan.next;
|
|
150
|
+
steps.push({
|
|
151
|
+
index,
|
|
152
|
+
before,
|
|
153
|
+
beforeConfiguration,
|
|
154
|
+
event,
|
|
155
|
+
plan,
|
|
156
|
+
after: current,
|
|
157
|
+
afterConfiguration: rawConfigurationPaths(machine, current)
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
scenario,
|
|
162
|
+
initial: initialTrace,
|
|
163
|
+
steps,
|
|
164
|
+
final: current,
|
|
165
|
+
finalConfiguration: rawConfigurationPaths(machine, current)
|
|
166
|
+
};
|
|
167
|
+
});
|
|
168
|
+
const canonicalize = (value, active) => {
|
|
169
|
+
if (value === undefined)
|
|
170
|
+
return { $undefined: true };
|
|
171
|
+
if (typeof value === "bigint")
|
|
172
|
+
return { $bigint: String(value) };
|
|
173
|
+
if (typeof value === "symbol")
|
|
174
|
+
return { $symbol: String(value) };
|
|
175
|
+
if (typeof value === "function")
|
|
176
|
+
return { $function: value.name || "anonymous" };
|
|
177
|
+
if (typeof value !== "object" || value === null)
|
|
178
|
+
return value;
|
|
179
|
+
if (active.has(value))
|
|
180
|
+
return { $circular: true };
|
|
181
|
+
active.add(value);
|
|
182
|
+
let result;
|
|
183
|
+
if (value instanceof Error) {
|
|
184
|
+
result = {
|
|
185
|
+
$error: value.name,
|
|
186
|
+
message: value.message,
|
|
187
|
+
...Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalize(value[key], active)]))
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
else if (Array.isArray(value)) {
|
|
191
|
+
result = value.map((item) => canonicalize(item, active));
|
|
192
|
+
}
|
|
193
|
+
else if (value instanceof Date) {
|
|
194
|
+
result = { $date: value.toISOString() };
|
|
195
|
+
}
|
|
196
|
+
else if (value instanceof Map) {
|
|
197
|
+
result = {
|
|
198
|
+
$map: Array.from(value, ([key, item]) => [canonicalize(key, active), canonicalize(item, active)]).sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)))
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
else if (value instanceof Set) {
|
|
202
|
+
result = {
|
|
203
|
+
$set: Array.from(value, (item) => canonicalize(item, active)).sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)))
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
result = Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalize(value[key], active)]));
|
|
208
|
+
}
|
|
209
|
+
active.delete(value);
|
|
210
|
+
return result;
|
|
211
|
+
};
|
|
212
|
+
const formatValue = (value) => JSON.stringify(canonicalize(value, new WeakSet()));
|
|
213
|
+
const structuralFingerprint = (value) => {
|
|
214
|
+
const references = new WeakMap();
|
|
215
|
+
let nextReference = 0;
|
|
216
|
+
const visit = (current) => {
|
|
217
|
+
if (current === undefined)
|
|
218
|
+
return ["undefined"];
|
|
219
|
+
if (current === null)
|
|
220
|
+
return ["null"];
|
|
221
|
+
if (typeof current === "number") {
|
|
222
|
+
if (Number.isNaN(current))
|
|
223
|
+
return ["number", "NaN"];
|
|
224
|
+
if (Object.is(current, -0))
|
|
225
|
+
return ["number", "-0"];
|
|
226
|
+
return ["number", current];
|
|
227
|
+
}
|
|
228
|
+
if (typeof current === "bigint")
|
|
229
|
+
return ["bigint", String(current)];
|
|
230
|
+
if (typeof current === "symbol") {
|
|
231
|
+
return [
|
|
232
|
+
"symbol",
|
|
233
|
+
Symbol.keyFor(current) === undefined ? "local" : "global",
|
|
234
|
+
Symbol.keyFor(current) ?? current.description
|
|
235
|
+
];
|
|
236
|
+
}
|
|
237
|
+
if (typeof current === "function")
|
|
238
|
+
return ["function", current.name, current.length];
|
|
239
|
+
if (typeof current !== "object")
|
|
240
|
+
return [typeof current, current];
|
|
241
|
+
const existing = references.get(current);
|
|
242
|
+
if (existing !== undefined)
|
|
243
|
+
return ["reference", existing];
|
|
244
|
+
const reference = nextReference++;
|
|
245
|
+
references.set(current, reference);
|
|
246
|
+
if (Array.isArray(current))
|
|
247
|
+
return ["array", reference, current.map(visit)];
|
|
248
|
+
if (current instanceof Date)
|
|
249
|
+
return ["date", reference, current.getTime()];
|
|
250
|
+
if (current instanceof RegExp)
|
|
251
|
+
return ["regexp", reference, current.source, current.flags];
|
|
252
|
+
if (current instanceof ArrayBuffer) {
|
|
253
|
+
return ["array-buffer", reference, Array.from(new Uint8Array(current))];
|
|
254
|
+
}
|
|
255
|
+
if (typeof SharedArrayBuffer !== "undefined" && current instanceof SharedArrayBuffer) {
|
|
256
|
+
return ["shared-array-buffer", reference, Array.from(new Uint8Array(current))];
|
|
257
|
+
}
|
|
258
|
+
if (ArrayBuffer.isView(current)) {
|
|
259
|
+
return [
|
|
260
|
+
"array-buffer-view",
|
|
261
|
+
reference,
|
|
262
|
+
current.constructor.name,
|
|
263
|
+
current.byteOffset,
|
|
264
|
+
current.byteLength,
|
|
265
|
+
Array.from(new Uint8Array(current.buffer, current.byteOffset, current.byteLength))
|
|
266
|
+
];
|
|
267
|
+
}
|
|
268
|
+
if (current instanceof Error) {
|
|
269
|
+
return ["error", reference, current.name, current.message, visit(Object.fromEntries(Object.entries(current)))];
|
|
270
|
+
}
|
|
271
|
+
if (current instanceof Map) {
|
|
272
|
+
return ["map", reference, Array.from(current, ([key, item]) => [visit(key), visit(item)])];
|
|
273
|
+
}
|
|
274
|
+
if (current instanceof Set)
|
|
275
|
+
return ["set", reference, Array.from(current, visit)];
|
|
276
|
+
const stringKeys = Object.getOwnPropertyNames(current).sort();
|
|
277
|
+
const symbolKeys = Object.getOwnPropertySymbols(current).sort((left, right) => String(Symbol.keyFor(left) ?? left.description).localeCompare(String(Symbol.keyFor(right) ?? right.description)));
|
|
278
|
+
return [
|
|
279
|
+
"object",
|
|
280
|
+
reference,
|
|
281
|
+
Object.getPrototypeOf(current)?.constructor?.name ?? null,
|
|
282
|
+
stringKeys.map((key) => [key, visit(current[key])]),
|
|
283
|
+
symbolKeys.map((key) => [visit(key), visit(current[key])])
|
|
284
|
+
];
|
|
285
|
+
};
|
|
286
|
+
return JSON.stringify(visit(value));
|
|
287
|
+
};
|
|
288
|
+
const structurallyEqual = (left, right) => {
|
|
289
|
+
const leftToRight = new WeakMap();
|
|
290
|
+
const rightToLeft = new WeakMap();
|
|
291
|
+
const compare = (a, b) => {
|
|
292
|
+
if (Object.is(a, b))
|
|
293
|
+
return true;
|
|
294
|
+
if (typeof a !== typeof b || a === null || b === null)
|
|
295
|
+
return false;
|
|
296
|
+
if (typeof a !== "object" || typeof b !== "object")
|
|
297
|
+
return false;
|
|
298
|
+
const knownRight = leftToRight.get(a);
|
|
299
|
+
const knownLeft = rightToLeft.get(b);
|
|
300
|
+
if (knownRight !== undefined || knownLeft !== undefined)
|
|
301
|
+
return knownRight === b && knownLeft === a;
|
|
302
|
+
leftToRight.set(a, b);
|
|
303
|
+
rightToLeft.set(b, a);
|
|
304
|
+
if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b))
|
|
305
|
+
return false;
|
|
306
|
+
if (a instanceof Date && b instanceof Date)
|
|
307
|
+
return a.getTime() === b.getTime();
|
|
308
|
+
if (a instanceof RegExp && b instanceof RegExp)
|
|
309
|
+
return a.source === b.source && a.flags === b.flags;
|
|
310
|
+
if (a instanceof ArrayBuffer && b instanceof ArrayBuffer) {
|
|
311
|
+
return a.byteLength === b.byteLength &&
|
|
312
|
+
new Uint8Array(a).every((byte, index) => byte === new Uint8Array(b)[index]);
|
|
313
|
+
}
|
|
314
|
+
if (typeof SharedArrayBuffer !== "undefined" && a instanceof SharedArrayBuffer && b instanceof SharedArrayBuffer) {
|
|
315
|
+
return a.byteLength === b.byteLength &&
|
|
316
|
+
new Uint8Array(a).every((byte, index) => byte === new Uint8Array(b)[index]);
|
|
317
|
+
}
|
|
318
|
+
if (ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) {
|
|
319
|
+
if (a.constructor !== b.constructor || a.byteOffset !== b.byteOffset || a.byteLength !== b.byteLength) {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
const leftBytes = new Uint8Array(a.buffer, a.byteOffset, a.byteLength);
|
|
323
|
+
const rightBytes = new Uint8Array(b.buffer, b.byteOffset, b.byteLength);
|
|
324
|
+
return leftBytes.every((byte, index) => byte === rightBytes[index]);
|
|
325
|
+
}
|
|
326
|
+
if (a instanceof Error && b instanceof Error && (a.name !== b.name || a.message !== b.message))
|
|
327
|
+
return false;
|
|
328
|
+
if (a instanceof Map && b instanceof Map) {
|
|
329
|
+
if (a.size !== b.size)
|
|
330
|
+
return false;
|
|
331
|
+
const leftEntries = Array.from(a);
|
|
332
|
+
const rightEntries = Array.from(b);
|
|
333
|
+
return leftEntries.every(([key, item], index) => compare(key, rightEntries[index][0]) && compare(item, rightEntries[index][1]));
|
|
334
|
+
}
|
|
335
|
+
if (a instanceof Set && b instanceof Set) {
|
|
336
|
+
if (a.size !== b.size)
|
|
337
|
+
return false;
|
|
338
|
+
const rightValues = Array.from(b);
|
|
339
|
+
return Array.from(a).every((item, index) => compare(item, rightValues[index]));
|
|
340
|
+
}
|
|
341
|
+
if (Array.isArray(a) !== Array.isArray(b))
|
|
342
|
+
return false;
|
|
343
|
+
if (!Array.isArray(a) && Object.getPrototypeOf(a) !== Object.prototype && Object.getPrototypeOf(a) !== null) {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
const leftKeys = Reflect.ownKeys(a).sort((first, second) => String(first).localeCompare(String(second)));
|
|
347
|
+
const rightKeys = Reflect.ownKeys(b).sort((first, second) => String(first).localeCompare(String(second)));
|
|
348
|
+
return leftKeys.length === rightKeys.length &&
|
|
349
|
+
leftKeys.every((key, index) => Object.is(key, rightKeys[index]) && compare(a[key], b[rightKeys[index]]));
|
|
350
|
+
};
|
|
351
|
+
return compare(left, right);
|
|
352
|
+
};
|
|
353
|
+
const makeStructuralIdentityIndex = () => {
|
|
354
|
+
const buckets = new Map();
|
|
355
|
+
return (value) => {
|
|
356
|
+
const fingerprint = structuralFingerprint(value);
|
|
357
|
+
let bucket = buckets.get(fingerprint);
|
|
358
|
+
if (bucket === undefined)
|
|
359
|
+
buckets.set(fingerprint, bucket = []);
|
|
360
|
+
const existing = bucket.find((candidate) => structurallyEqual(candidate.value, value));
|
|
361
|
+
if (existing !== undefined)
|
|
362
|
+
return existing.id;
|
|
363
|
+
const id = bucket.length === 0 ? fingerprint : `${fingerprint}#collision:${bucket.length}`;
|
|
364
|
+
bucket.push({ id, value });
|
|
365
|
+
return id;
|
|
366
|
+
};
|
|
367
|
+
};
|
|
368
|
+
const coverageSummary = (declared, hit) => {
|
|
369
|
+
const hits = [];
|
|
370
|
+
const misses = [];
|
|
371
|
+
declared.forEach((item, index) => (hit.has(index) ? hits : misses).push(item));
|
|
372
|
+
return {
|
|
373
|
+
total: declared.length,
|
|
374
|
+
hit: hits.length,
|
|
375
|
+
missing: misses.length,
|
|
376
|
+
hits,
|
|
377
|
+
misses
|
|
378
|
+
};
|
|
379
|
+
};
|
|
380
|
+
const normalizeTraces = (traceOrTraces) => Array.isArray(traceOrTraces) ? traceOrTraces : [traceOrTraces];
|
|
381
|
+
const sameCoverageTrigger = (left, right) => left.type === right.type && (left.type !== "event" || right.type === "event" && left.event === right.event);
|
|
382
|
+
const targetWithinDeclaredBounds = (target, bounds) => target === undefined || bounds.type === "dynamic" ||
|
|
383
|
+
bounds.paths.some((path) => target === path || target.startsWith(`${path}.`));
|
|
384
|
+
const finiteTagValues = (ast) => {
|
|
385
|
+
if (SchemaAST.isLiteral(ast)) {
|
|
386
|
+
return typeof ast.literal === "string" || typeof ast.literal === "number" ? [ast.literal] : undefined;
|
|
387
|
+
}
|
|
388
|
+
if (SchemaAST.isUniqueSymbol(ast))
|
|
389
|
+
return [ast.symbol];
|
|
390
|
+
if (SchemaAST.isUnion(ast)) {
|
|
391
|
+
const values = [];
|
|
392
|
+
for (const member of ast.types) {
|
|
393
|
+
const memberValues = finiteTagValues(member);
|
|
394
|
+
if (memberValues === undefined)
|
|
395
|
+
return undefined;
|
|
396
|
+
for (const value of memberValues)
|
|
397
|
+
if (!values.includes(value))
|
|
398
|
+
values.push(value);
|
|
399
|
+
}
|
|
400
|
+
return values;
|
|
401
|
+
}
|
|
402
|
+
if (SchemaAST.isObjects(ast)) {
|
|
403
|
+
const tag = ast.propertySignatures.find(({ name }) => name === "_tag")?.type;
|
|
404
|
+
return tag === undefined ? undefined : finiteTagValues(tag);
|
|
405
|
+
}
|
|
406
|
+
if (SchemaAST.isDeclaration(ast)) {
|
|
407
|
+
const sentinels = ast.annotations?.["~sentinels"];
|
|
408
|
+
if (Array.isArray(sentinels)) {
|
|
409
|
+
const tag = sentinels.find((sentinel) => typeof sentinel === "object" && sentinel !== null && sentinel.key === "_tag" &&
|
|
410
|
+
(typeof sentinel.literal === "string" ||
|
|
411
|
+
typeof sentinel.literal === "number" ||
|
|
412
|
+
typeof sentinel.literal === "symbol"));
|
|
413
|
+
if (tag !== undefined)
|
|
414
|
+
return [tag.literal];
|
|
415
|
+
}
|
|
416
|
+
for (const parameter of ast.typeParameters) {
|
|
417
|
+
const values = finiteTagValues(parameter);
|
|
418
|
+
if (values !== undefined)
|
|
419
|
+
return values;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
if (SchemaAST.isSuspend(ast))
|
|
423
|
+
return finiteTagValues(ast.thunk());
|
|
424
|
+
return undefined;
|
|
425
|
+
};
|
|
426
|
+
const publicEventTags = (machine) => {
|
|
427
|
+
const tags = [];
|
|
428
|
+
const diagnostics = [];
|
|
429
|
+
machine.events.forEach((schema, schemaIndex) => {
|
|
430
|
+
const values = finiteTagValues(SchemaAST.toType(schema.ast));
|
|
431
|
+
if (values === undefined) {
|
|
432
|
+
diagnostics.push({
|
|
433
|
+
schemaIndex,
|
|
434
|
+
message: "The decoded _tag schema is not a finite literal, unique symbol, or finite union"
|
|
435
|
+
});
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
for (const value of values) {
|
|
439
|
+
const tag = value;
|
|
440
|
+
if (!tags.includes(tag))
|
|
441
|
+
tags.push(tag);
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
return { tags, diagnostics };
|
|
445
|
+
};
|
|
446
|
+
export const coverage = (machine, traceOrTraces) => {
|
|
447
|
+
const traces = normalizeTraces(traceOrTraces);
|
|
448
|
+
const stateNodes = Machine.stateNodes(machine);
|
|
449
|
+
const activeNodes = stateNodes.filter((node) => node.type !== "history" && node.type !== "choice").map((node) => ({
|
|
450
|
+
path: node.path,
|
|
451
|
+
type: node.type
|
|
452
|
+
}));
|
|
453
|
+
const activeIndex = new Map(activeNodes.map((node, index) => [node.path, index]));
|
|
454
|
+
const activationHits = new Set();
|
|
455
|
+
const entryHits = new Set();
|
|
456
|
+
const exitHits = new Set();
|
|
457
|
+
const definitions = Machine.transitionDefinitions(machine).map((definition, index) => ({
|
|
458
|
+
id: `transition:${index}:${formatValue(definition)}`,
|
|
459
|
+
index,
|
|
460
|
+
source: definition.source,
|
|
461
|
+
trigger: definition.trigger,
|
|
462
|
+
reenter: definition.reenter,
|
|
463
|
+
targets: definition.targets
|
|
464
|
+
}));
|
|
465
|
+
const transitionHits = new Set();
|
|
466
|
+
const declaredEvents = publicEventTags(machine);
|
|
467
|
+
const declaredEventTags = declaredEvents.tags;
|
|
468
|
+
const eventCounts = new Map(declaredEventTags.map((tag) => [tag, 0]));
|
|
469
|
+
const logicalConfigurationIdentities = new Set();
|
|
470
|
+
const logicalConfigurationIdentity = makeStructuralIdentityIndex();
|
|
471
|
+
let configurationObservations = 0;
|
|
472
|
+
let scenarioEvents = 0;
|
|
473
|
+
let emptyScenarios = 0;
|
|
474
|
+
let startupWithMicrosteps = 0;
|
|
475
|
+
let microsteps = 0;
|
|
476
|
+
let changedMicrosteps = 0;
|
|
477
|
+
let targetlessTransitions = 0;
|
|
478
|
+
let raisedEvents = 0;
|
|
479
|
+
let emittedEvents = 0;
|
|
480
|
+
let eventTriggered = 0;
|
|
481
|
+
let alwaysTriggered = 0;
|
|
482
|
+
let doneTriggered = 0;
|
|
483
|
+
let choiceTriggered = 0;
|
|
484
|
+
let donePlans = 0;
|
|
485
|
+
let completionRecordObservations = 0;
|
|
486
|
+
const completionPaths = new Set();
|
|
487
|
+
let historyRecordObservations = 0;
|
|
488
|
+
const historyModes = new Map();
|
|
489
|
+
let historyTargets = 0;
|
|
490
|
+
let resolvedHistoryTargets = 0;
|
|
491
|
+
const nodeByPath = new Map(stateNodes.map((node) => [node.path, node]));
|
|
492
|
+
const hitPaths = (paths, hits) => {
|
|
493
|
+
for (const path of paths) {
|
|
494
|
+
const index = activeIndex.get(path);
|
|
495
|
+
if (index !== undefined)
|
|
496
|
+
hits.add(index);
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
const observeSnapshot = (snapshot) => {
|
|
500
|
+
const paths = rawConfigurationPaths(machine, snapshot);
|
|
501
|
+
hitPaths(paths, activationHits);
|
|
502
|
+
configurationObservations += 1;
|
|
503
|
+
logicalConfigurationIdentities.add(logicalConfigurationIdentity(snapshot));
|
|
504
|
+
const metadata = snapshot;
|
|
505
|
+
for (const completion of metadata.completed ?? []) {
|
|
506
|
+
completionRecordObservations += 1;
|
|
507
|
+
completionPaths.add(completion.path);
|
|
508
|
+
}
|
|
509
|
+
for (const [path, record] of Object.entries(metadata.history ?? {})) {
|
|
510
|
+
historyRecordObservations += 1;
|
|
511
|
+
let modes = historyModes.get(path);
|
|
512
|
+
if (modes === undefined)
|
|
513
|
+
historyModes.set(path, modes = new Set());
|
|
514
|
+
modes.add(record.mode);
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
const observeMicrostep = (microstep) => {
|
|
518
|
+
microsteps += 1;
|
|
519
|
+
if (microstep.changed)
|
|
520
|
+
changedMicrosteps += 1;
|
|
521
|
+
raisedEvents += microstep.raisedEvents.length;
|
|
522
|
+
emittedEvents += microstep.emittedEvents.length;
|
|
523
|
+
hitPaths(microstep.entryPaths, entryHits);
|
|
524
|
+
hitPaths(microstep.exitPaths, exitHits);
|
|
525
|
+
observeSnapshot(microstep.next);
|
|
526
|
+
for (const retained of microstep.transitions) {
|
|
527
|
+
if (retained.target === undefined)
|
|
528
|
+
targetlessTransitions += 1;
|
|
529
|
+
if (retained.trigger.type === "event")
|
|
530
|
+
eventTriggered += 1;
|
|
531
|
+
else if (retained.trigger.type === "always")
|
|
532
|
+
alwaysTriggered += 1;
|
|
533
|
+
else if (retained.trigger.type === "done")
|
|
534
|
+
doneTriggered += 1;
|
|
535
|
+
else
|
|
536
|
+
choiceTriggered += 1;
|
|
537
|
+
if (retained.target !== undefined && nodeByPath.get(retained.target)?.type === "history") {
|
|
538
|
+
historyTargets += 1;
|
|
539
|
+
if (retained.resolvedTarget !== undefined)
|
|
540
|
+
resolvedHistoryTargets += 1;
|
|
541
|
+
}
|
|
542
|
+
const definitionIndex = definitions.findIndex((definition) => definition.source === retained.source &&
|
|
543
|
+
definition.reenter === retained.reenter &&
|
|
544
|
+
sameCoverageTrigger(definition.trigger, retained.trigger) &&
|
|
545
|
+
targetWithinDeclaredBounds(retained.target, definition.targets));
|
|
546
|
+
if (definitionIndex !== -1)
|
|
547
|
+
transitionHits.add(definitionIndex);
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
for (const trace of traces) {
|
|
551
|
+
scenarioEvents += trace.scenario.events.length;
|
|
552
|
+
if (trace.scenario.events.length === 0)
|
|
553
|
+
emptyScenarios += 1;
|
|
554
|
+
for (const event of trace.scenario.events) {
|
|
555
|
+
const tag = event._tag;
|
|
556
|
+
eventCounts.set(tag, (eventCounts.get(tag) ?? 0) + 1);
|
|
557
|
+
}
|
|
558
|
+
observeSnapshot(trace.initial.startingState);
|
|
559
|
+
hitPaths(trace.initial.initialEntryPaths, entryHits);
|
|
560
|
+
if (trace.initial.plan.microsteps.length > 0)
|
|
561
|
+
startupWithMicrosteps += 1;
|
|
562
|
+
for (const microstep of trace.initial.plan.microsteps)
|
|
563
|
+
observeMicrostep(microstep);
|
|
564
|
+
observeSnapshot(trace.initial.plan.state);
|
|
565
|
+
if (trace.initial.plan.done)
|
|
566
|
+
donePlans += 1;
|
|
567
|
+
for (const step of trace.steps) {
|
|
568
|
+
observeSnapshot(step.before);
|
|
569
|
+
for (const microstep of step.plan.microsteps)
|
|
570
|
+
observeMicrostep(microstep);
|
|
571
|
+
observeSnapshot(step.after);
|
|
572
|
+
if (step.plan.done)
|
|
573
|
+
donePlans += 1;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
const eventItems = declaredEventTags.map((tag) => ({
|
|
577
|
+
tag,
|
|
578
|
+
count: eventCounts.get(tag) ?? 0
|
|
579
|
+
}));
|
|
580
|
+
const eventHits = eventItems.filter(({ count }) => count > 0);
|
|
581
|
+
const eventMisses = eventItems.filter(({ count }) => count === 0);
|
|
582
|
+
return {
|
|
583
|
+
states: {
|
|
584
|
+
activation: coverageSummary(activeNodes, activationHits),
|
|
585
|
+
entry: coverageSummary(activeNodes, entryHits),
|
|
586
|
+
exit: coverageSummary(activeNodes, exitHits)
|
|
587
|
+
},
|
|
588
|
+
transitions: coverageSummary(definitions, transitionHits),
|
|
589
|
+
events: declaredEvents.diagnostics.length === 0
|
|
590
|
+
? {
|
|
591
|
+
available: true,
|
|
592
|
+
total: eventItems.length,
|
|
593
|
+
hit: eventHits.length,
|
|
594
|
+
missing: eventMisses.length,
|
|
595
|
+
hits: eventHits,
|
|
596
|
+
misses: eventMisses,
|
|
597
|
+
observed: eventItems,
|
|
598
|
+
diagnostics: []
|
|
599
|
+
}
|
|
600
|
+
: {
|
|
601
|
+
available: false,
|
|
602
|
+
total: undefined,
|
|
603
|
+
hit: undefined,
|
|
604
|
+
missing: undefined,
|
|
605
|
+
hits: undefined,
|
|
606
|
+
misses: undefined,
|
|
607
|
+
observed: Array.from(eventCounts, ([tag, count]) => ({ tag, count })),
|
|
608
|
+
diagnostics: declaredEvents.diagnostics
|
|
609
|
+
},
|
|
610
|
+
scenarios: {
|
|
611
|
+
traces: traces.length,
|
|
612
|
+
events: scenarioEvents,
|
|
613
|
+
empty: emptyScenarios
|
|
614
|
+
},
|
|
615
|
+
logicalConfigurations: {
|
|
616
|
+
observations: configurationObservations,
|
|
617
|
+
hit: logicalConfigurationIdentities.size,
|
|
618
|
+
identities: Array.from(logicalConfigurationIdentities).sort()
|
|
619
|
+
},
|
|
620
|
+
startup: {
|
|
621
|
+
traces: traces.length,
|
|
622
|
+
withMicrosteps: startupWithMicrosteps
|
|
623
|
+
},
|
|
624
|
+
microsteps: {
|
|
625
|
+
total: microsteps,
|
|
626
|
+
changed: changedMicrosteps,
|
|
627
|
+
targetless: targetlessTransitions,
|
|
628
|
+
raised: raisedEvents,
|
|
629
|
+
emitted: emittedEvents,
|
|
630
|
+
eventTriggered,
|
|
631
|
+
alwaysTriggered,
|
|
632
|
+
doneTriggered,
|
|
633
|
+
choiceTriggered
|
|
634
|
+
},
|
|
635
|
+
completion: {
|
|
636
|
+
donePlans,
|
|
637
|
+
recordObservations: completionRecordObservations,
|
|
638
|
+
paths: Array.from(completionPaths).sort()
|
|
639
|
+
},
|
|
640
|
+
history: {
|
|
641
|
+
recordObservations: historyRecordObservations,
|
|
642
|
+
recorded: Array.from(historyModes, ([path, modes]) => ({
|
|
643
|
+
path: path,
|
|
644
|
+
modes: Array.from(modes).sort()
|
|
645
|
+
})).sort((left, right) => left.path.localeCompare(right.path)),
|
|
646
|
+
targets: historyTargets,
|
|
647
|
+
resolvedTargets: resolvedHistoryTargets
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
};
|
|
651
|
+
export const observedGraph = Effect.fnUntraced(function* (machine, traceOrTraces) {
|
|
652
|
+
const traces = normalizeTraces(traceOrTraces);
|
|
653
|
+
const occurrences = [];
|
|
654
|
+
const edgeDrafts = [];
|
|
655
|
+
const startOccurrences = [];
|
|
656
|
+
const startupSourceOccurrences = [];
|
|
657
|
+
const addOccurrence = (snapshot, role) => {
|
|
658
|
+
const index = occurrences.length;
|
|
659
|
+
occurrences.push({ snapshot, role });
|
|
660
|
+
return index;
|
|
661
|
+
};
|
|
662
|
+
const addMicrosteps = (microsteps) => microsteps.map((microstep) => ({
|
|
663
|
+
nextOccurrence: addOccurrence(microstep.next, "microstep"),
|
|
664
|
+
microstep
|
|
665
|
+
}));
|
|
666
|
+
traces.forEach((trace, traceIndex) => {
|
|
667
|
+
const start = addOccurrence(trace.initial.startingState, "startup");
|
|
668
|
+
startupSourceOccurrences.push(start);
|
|
669
|
+
const startupMicrosteps = addMicrosteps(trace.initial.plan.microsteps);
|
|
670
|
+
const initialized = addOccurrence(trace.initial.plan.state, "startup");
|
|
671
|
+
startOccurrences.push(initialized);
|
|
672
|
+
edgeDrafts.push({
|
|
673
|
+
sourceOccurrence: start,
|
|
674
|
+
targetOccurrence: initialized,
|
|
675
|
+
edge: {
|
|
676
|
+
_tag: "Startup",
|
|
677
|
+
traceIndex,
|
|
678
|
+
microsteps: startupMicrosteps,
|
|
679
|
+
completion: trace.initial.plan.done
|
|
680
|
+
? { done: true, output: trace.initial.plan.output }
|
|
681
|
+
: { done: false, output: undefined }
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
for (const step of trace.steps) {
|
|
685
|
+
const before = addOccurrence(step.before, "event");
|
|
686
|
+
const stepMicrosteps = addMicrosteps(step.plan.microsteps);
|
|
687
|
+
const after = addOccurrence(step.after, "event");
|
|
688
|
+
edgeDrafts.push({
|
|
689
|
+
sourceOccurrence: before,
|
|
690
|
+
targetOccurrence: after,
|
|
691
|
+
edge: {
|
|
692
|
+
_tag: "Event",
|
|
693
|
+
traceIndex,
|
|
694
|
+
eventIndex: step.index,
|
|
695
|
+
event: step.event,
|
|
696
|
+
microsteps: stepMicrosteps,
|
|
697
|
+
completion: step.plan.done
|
|
698
|
+
? { done: true, output: step.plan.output }
|
|
699
|
+
: { done: false, output: undefined }
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
const encoded = yield* Effect.forEach(occurrences, ({ snapshot }) => Machine.encodeSnapshot(machine, snapshot));
|
|
705
|
+
const encodedIdentity = makeStructuralIdentityIndex();
|
|
706
|
+
const occurrenceIds = encoded.map(encodedIdentity);
|
|
707
|
+
const grouped = new Map();
|
|
708
|
+
occurrences.forEach((occurrence, index) => {
|
|
709
|
+
const id = occurrenceIds[index];
|
|
710
|
+
let group = grouped.get(id);
|
|
711
|
+
if (group === undefined) {
|
|
712
|
+
grouped.set(id, group = {
|
|
713
|
+
encoded: encoded[index],
|
|
714
|
+
snapshot: occurrence.snapshot,
|
|
715
|
+
startup: 0,
|
|
716
|
+
event: 0,
|
|
717
|
+
microstep: 0
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
group[occurrence.role] += 1;
|
|
721
|
+
});
|
|
722
|
+
const nodesById = new Map();
|
|
723
|
+
const graph = Graph.directed((mutable) => {
|
|
724
|
+
for (const [id, group] of grouped) {
|
|
725
|
+
const node = Graph.addNode(mutable, {
|
|
726
|
+
id,
|
|
727
|
+
snapshot: group.snapshot,
|
|
728
|
+
encoded: group.encoded,
|
|
729
|
+
configuration: group.encoded.active.map(({ path }) => path),
|
|
730
|
+
observations: {
|
|
731
|
+
total: group.startup + group.event + group.microstep,
|
|
732
|
+
startup: group.startup,
|
|
733
|
+
event: group.event,
|
|
734
|
+
microstep: group.microstep
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
nodesById.set(id, node);
|
|
738
|
+
}
|
|
739
|
+
for (const draft of edgeDrafts) {
|
|
740
|
+
const source = nodesById.get(occurrenceIds[draft.sourceOccurrence]);
|
|
741
|
+
const target = nodesById.get(occurrenceIds[draft.targetOccurrence]);
|
|
742
|
+
Graph.addEdge(mutable, source, target, {
|
|
743
|
+
...draft.edge,
|
|
744
|
+
microsteps: draft.edge.microsteps.map(({ microstep, nextOccurrence }) => ({
|
|
745
|
+
next: occurrenceIds[nextOccurrence],
|
|
746
|
+
event: microstep.event,
|
|
747
|
+
transitions: microstep.transitions,
|
|
748
|
+
raisedEvents: microstep.raisedEvents,
|
|
749
|
+
emittedEvents: microstep.emittedEvents,
|
|
750
|
+
exitPaths: microstep.exitPaths,
|
|
751
|
+
entryPaths: microstep.entryPaths,
|
|
752
|
+
changed: microstep.changed
|
|
753
|
+
}))
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
return {
|
|
758
|
+
graph,
|
|
759
|
+
nodesById,
|
|
760
|
+
starts: Array.from(new Set(startOccurrences.map((index) => nodesById.get(occurrenceIds[index])))),
|
|
761
|
+
startupSources: Array.from(new Set(startupSourceOccurrences.map((index) => nodesById.get(occurrenceIds[index]))))
|
|
762
|
+
};
|
|
763
|
+
});
|
|
764
|
+
export class VerificationError extends Data.TaggedError("MachineTestVerificationError") {
|
|
765
|
+
}
|
|
766
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
767
|
+
const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
|
|
768
|
+
const sameValue = (left, right) => formatValue(left) === formatValue(right);
|
|
769
|
+
const samePaths = (left, right) => left.length === right.length && left.every((path, index) => path === right[index]);
|
|
770
|
+
const sameTrigger = (left, right) => left.type === right.type && (left.type !== "event" || right.type === "event" && left.event === right.event);
|
|
771
|
+
const makeNodeUtilities = (nodes) => {
|
|
772
|
+
const byPath = new Map(nodes.map((node) => [node.path, node]));
|
|
773
|
+
const depth = (path) => {
|
|
774
|
+
let current = byPath.get(path);
|
|
775
|
+
let result = 0;
|
|
776
|
+
const seen = new Set();
|
|
777
|
+
while (current !== undefined && !seen.has(current.path)) {
|
|
778
|
+
seen.add(current.path);
|
|
779
|
+
result += 1;
|
|
780
|
+
current = current.parent === undefined ? undefined : byPath.get(current.parent);
|
|
781
|
+
}
|
|
782
|
+
return result;
|
|
783
|
+
};
|
|
784
|
+
const isDescendantOrSelf = (path, ancestor) => {
|
|
785
|
+
let current = byPath.get(path);
|
|
786
|
+
const seen = new Set();
|
|
787
|
+
while (current !== undefined && !seen.has(current.path)) {
|
|
788
|
+
if (current.path === ancestor)
|
|
789
|
+
return true;
|
|
790
|
+
seen.add(current.path);
|
|
791
|
+
current = current.parent === undefined ? undefined : byPath.get(current.parent);
|
|
792
|
+
}
|
|
793
|
+
return false;
|
|
794
|
+
};
|
|
795
|
+
const ancestors = (path) => {
|
|
796
|
+
const result = [];
|
|
797
|
+
let current = byPath.get(path);
|
|
798
|
+
const seen = new Set();
|
|
799
|
+
while (current !== undefined && !seen.has(current.path)) {
|
|
800
|
+
seen.add(current.path);
|
|
801
|
+
result.unshift(current.path);
|
|
802
|
+
current = current.parent === undefined ? undefined : byPath.get(current.parent);
|
|
803
|
+
}
|
|
804
|
+
return result;
|
|
805
|
+
};
|
|
806
|
+
return { ancestors, byPath, depth, isDescendantOrSelf };
|
|
807
|
+
};
|
|
808
|
+
export const verify = (machine, trace, options = {}) => {
|
|
809
|
+
const selected = new Set(options.laws ?? ["configuration", "microsteps", "completion", "history", "targetBounds"]);
|
|
810
|
+
const nodes = Machine.stateNodes(machine);
|
|
811
|
+
const definitions = Machine.transitionDefinitions(machine);
|
|
812
|
+
const { ancestors, byPath, depth, isDescendantOrSelf } = makeNodeUtilities(nodes);
|
|
813
|
+
const violations = [];
|
|
814
|
+
const add = (law, location, message, path) => {
|
|
815
|
+
violations.push({
|
|
816
|
+
law,
|
|
817
|
+
eventIndex: location.eventIndex,
|
|
818
|
+
...(location.microstepIndex === undefined ? {} : { microstepIndex: location.microstepIndex }),
|
|
819
|
+
...(path === undefined ? {} : { path }),
|
|
820
|
+
message
|
|
821
|
+
});
|
|
822
|
+
};
|
|
823
|
+
const schemaMatches = (schema, value) => {
|
|
824
|
+
if (schema === undefined)
|
|
825
|
+
return false;
|
|
826
|
+
try {
|
|
827
|
+
return Schema.is(schema)(value);
|
|
828
|
+
}
|
|
829
|
+
catch {
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
const inspectSnapshot = (snapshot, location, label) => {
|
|
834
|
+
const active = new Set();
|
|
835
|
+
const paths = [];
|
|
836
|
+
const values = new Map();
|
|
837
|
+
const reportConfiguration = selected.has("configuration");
|
|
838
|
+
const root = isRecord(snapshot) ? snapshot : undefined;
|
|
839
|
+
const visit = (current, expectedParent, expectedPath) => {
|
|
840
|
+
if (!isRecord(current)) {
|
|
841
|
+
if (reportConfiguration) {
|
|
842
|
+
add("configuration.shape", location, `${label} must contain an object snapshot`);
|
|
843
|
+
}
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
if (typeof current.path !== "string") {
|
|
847
|
+
if (reportConfiguration) {
|
|
848
|
+
add("configuration.path", location, `${label} contains a snapshot without a string path`);
|
|
849
|
+
}
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
const path = current.path;
|
|
853
|
+
if (active.has(path)) {
|
|
854
|
+
if (reportConfiguration) {
|
|
855
|
+
add("configuration.duplicate", location, `${label} activates state "${path}" more than once`, path);
|
|
856
|
+
}
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
active.add(path);
|
|
860
|
+
paths.push(path);
|
|
861
|
+
values.set(path, current.value);
|
|
862
|
+
const node = byPath.get(path);
|
|
863
|
+
if (node === undefined) {
|
|
864
|
+
if (reportConfiguration) {
|
|
865
|
+
add("configuration.path", location, `${label} activates unknown state "${path}"`, path);
|
|
866
|
+
}
|
|
867
|
+
if (isRecord(current.state))
|
|
868
|
+
visit(current.state, path);
|
|
869
|
+
if (isRecord(current.states)) {
|
|
870
|
+
for (const child of Object.values(current.states))
|
|
871
|
+
visit(child, path);
|
|
872
|
+
}
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
if (expectedPath !== undefined && path !== expectedPath && reportConfiguration) {
|
|
876
|
+
add("configuration.hierarchy", location, `${label} expected region "${expectedPath}" but found "${path}"`, path);
|
|
877
|
+
}
|
|
878
|
+
if (node.parent !== expectedParent && reportConfiguration) {
|
|
879
|
+
add("configuration.hierarchy", location, expectedParent === undefined
|
|
880
|
+
? `${label} root state "${path}" is not a machine root`
|
|
881
|
+
: `${label} state "${path}" is not a direct child of "${expectedParent}"`, path);
|
|
882
|
+
}
|
|
883
|
+
if (node.type === "history" || node.type === "choice") {
|
|
884
|
+
if (reportConfiguration) {
|
|
885
|
+
add("configuration.path", location, `${label} activates ${node.type} pseudo-state "${path}"`, path);
|
|
886
|
+
}
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
if (reportConfiguration && !schemaMatches(node.schema, current.value)) {
|
|
890
|
+
add("configuration.schema", location, `${label} value for "${path}" does not match its schema`, path);
|
|
891
|
+
}
|
|
892
|
+
if (node.type === "compound") {
|
|
893
|
+
if (!isRecord(current.state)) {
|
|
894
|
+
if (reportConfiguration) {
|
|
895
|
+
add("configuration.compound", location, `${label} compound state "${path}" must have exactly one child`, path);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
else {
|
|
899
|
+
visit(current.state, path);
|
|
900
|
+
}
|
|
901
|
+
if (hasOwn(current, "states") && reportConfiguration) {
|
|
902
|
+
add("configuration.compound", location, `${label} compound state "${path}" contains parallel regions`, path);
|
|
903
|
+
}
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
if (node.type === "parallel") {
|
|
907
|
+
if (!isRecord(current.states)) {
|
|
908
|
+
if (reportConfiguration) {
|
|
909
|
+
add("configuration.parallel", location, `${label} parallel state "${path}" has no region map`, path);
|
|
910
|
+
}
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
const expectedKeys = new Set();
|
|
914
|
+
for (const childPath of node.children) {
|
|
915
|
+
const child = byPath.get(childPath);
|
|
916
|
+
if (child === undefined)
|
|
917
|
+
continue;
|
|
918
|
+
expectedKeys.add(child.key);
|
|
919
|
+
if (!hasOwn(current.states, child.key)) {
|
|
920
|
+
if (reportConfiguration) {
|
|
921
|
+
add("configuration.parallel", location, `${label} parallel state "${path}" omits region "${child.key}"`, childPath);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
else {
|
|
925
|
+
visit(current.states[child.key], path, child.path);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
for (const key of Object.keys(current.states)) {
|
|
929
|
+
if (!expectedKeys.has(key)) {
|
|
930
|
+
if (reportConfiguration) {
|
|
931
|
+
add("configuration.parallel", location, `${label} parallel state "${path}" contains extra region "${key}"`, path);
|
|
932
|
+
}
|
|
933
|
+
visit(current.states[key], path);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
if (hasOwn(current, "state") && reportConfiguration) {
|
|
937
|
+
add("configuration.parallel", location, `${label} parallel state "${path}" contains a compound child`, path);
|
|
938
|
+
}
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
if ((hasOwn(current, "state") || hasOwn(current, "states")) && reportConfiguration) {
|
|
942
|
+
add("configuration.shape", location, `${label} leaf state "${path}" contains active children`, path);
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
visit(snapshot, undefined);
|
|
946
|
+
return { active, paths, values, root };
|
|
947
|
+
};
|
|
948
|
+
const validateTraceConfiguration = (expected, actual, location, label) => {
|
|
949
|
+
if (selected.has("configuration") && !samePaths(expected.paths, actual)) {
|
|
950
|
+
add("configuration.trace", location, `${label} configuration [${actual.join(", ")}] does not match snapshot [${expected.paths.join(", ")}]`);
|
|
951
|
+
}
|
|
952
|
+
};
|
|
953
|
+
const validateHistory = (snapshot, location, label) => {
|
|
954
|
+
if (!selected.has("history") || snapshot.root === undefined || !hasOwn(snapshot.root, "history"))
|
|
955
|
+
return;
|
|
956
|
+
const history = snapshot.root.history;
|
|
957
|
+
if (!isRecord(history)) {
|
|
958
|
+
add("history.record", location, `${label} history metadata must be a record`);
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
for (const [historyPath, unknownEntry] of Object.entries(history)) {
|
|
962
|
+
const historyNode = byPath.get(historyPath);
|
|
963
|
+
if (historyNode === undefined || historyNode.type !== "history" || historyNode.parent === undefined) {
|
|
964
|
+
add("history.path", location, `${label} contains unknown history record "${historyPath}"`, historyPath);
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
if (!isRecord(unknownEntry)) {
|
|
968
|
+
add("history.record", location, `${label} history record "${historyPath}" must be an object`, historyPath);
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
const entry = unknownEntry;
|
|
972
|
+
if (entry.mode !== historyNode.history) {
|
|
973
|
+
add("history.mode", location, `${label} history record "${historyPath}" has mode "${String(entry.mode)}", expected "${historyNode.history}"`, historyPath);
|
|
974
|
+
}
|
|
975
|
+
if (!Array.isArray(entry.active) || !entry.active.every((path) => typeof path === "string")) {
|
|
976
|
+
add("history.record", location, `${label} history record "${historyPath}" must contain string paths`, historyPath);
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
if (!isRecord(entry.values)) {
|
|
980
|
+
add("history.record", location, `${label} history record "${historyPath}" must contain a values record`, historyPath);
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
983
|
+
const rememberedPaths = entry.active;
|
|
984
|
+
const remembered = new Set();
|
|
985
|
+
for (const path of rememberedPaths) {
|
|
986
|
+
if (remembered.has(path)) {
|
|
987
|
+
add("history.path", location, `${label} history record "${historyPath}" repeats "${path}"`, path);
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
remembered.add(path);
|
|
991
|
+
const node = byPath.get(path);
|
|
992
|
+
if (node === undefined || node.type === "history" || node.type === "choice") {
|
|
993
|
+
add("history.path", location, `${label} history record "${historyPath}" contains invalid state "${path}"`, path);
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
const inOwnerSubtree = isDescendantOrSelf(path, historyNode.parent);
|
|
997
|
+
const inOwnerAncestry = ancestors(historyNode.parent).includes(path);
|
|
998
|
+
if (!inOwnerSubtree && !inOwnerAncestry) {
|
|
999
|
+
add("history.path", location, `${label} history record "${historyPath}" contains state "${path}" outside its owner`, path);
|
|
1000
|
+
}
|
|
1001
|
+
if (!hasOwn(entry.values, path)) {
|
|
1002
|
+
add("history.value", location, `${label} history record "${historyPath}" omits value for "${path}"`, path);
|
|
1003
|
+
}
|
|
1004
|
+
else if (!schemaMatches(node.schema, entry.values[path])) {
|
|
1005
|
+
add("history.value", location, `${label} history value for "${path}" does not match its state schema`, path);
|
|
1006
|
+
}
|
|
1007
|
+
if (entry.mode === "shallow" && isDescendantOrSelf(path, historyNode.parent) && path !== historyNode.parent &&
|
|
1008
|
+
node.parent !== historyNode.parent) {
|
|
1009
|
+
add("history.shallow", location, `${label} shallow history record "${historyPath}" contains deep descendant "${path}"`, path);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
for (const path of Object.keys(entry.values)) {
|
|
1013
|
+
if (!remembered.has(path)) {
|
|
1014
|
+
add("history.value", location, `${label} history record "${historyPath}" has extra value "${path}"`, path);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
if (entry.mode === "deep") {
|
|
1018
|
+
for (const path of remembered) {
|
|
1019
|
+
if (!isDescendantOrSelf(path, historyNode.parent) || path === historyNode.parent)
|
|
1020
|
+
continue;
|
|
1021
|
+
let parent = byPath.get(path)?.parent;
|
|
1022
|
+
while (parent !== undefined && isDescendantOrSelf(parent, historyNode.parent)) {
|
|
1023
|
+
if (!remembered.has(parent)) {
|
|
1024
|
+
add("history.deep", location, `${label} deep history record "${historyPath}" remembers "${path}" without ancestor "${parent}"`, path);
|
|
1025
|
+
}
|
|
1026
|
+
if (parent === historyNode.parent)
|
|
1027
|
+
break;
|
|
1028
|
+
parent = byPath.get(parent)?.parent;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
for (const ancestor of ancestors(historyNode.parent)) {
|
|
1033
|
+
if (!remembered.has(ancestor)) {
|
|
1034
|
+
add("history.path", location, `${label} history record "${historyPath}" omits owner ancestry state "${ancestor}"`, ancestor);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
const validateRememberedControl = (path, recurse) => {
|
|
1038
|
+
const node = byPath.get(path);
|
|
1039
|
+
if (node === undefined)
|
|
1040
|
+
return;
|
|
1041
|
+
const activeChildren = node.children.filter((child) => remembered.has(child));
|
|
1042
|
+
if (node.type === "compound") {
|
|
1043
|
+
if (activeChildren.length !== 1) {
|
|
1044
|
+
add(entry.mode === "deep" ? "history.deep" : "history.shallow", location, `${label} history record "${historyPath}" must remember one child of compound state "${path}"`, path);
|
|
1045
|
+
}
|
|
1046
|
+
else if (recurse) {
|
|
1047
|
+
validateRememberedControl(activeChildren[0], true);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
else if (node.type === "parallel") {
|
|
1051
|
+
for (const child of node.children) {
|
|
1052
|
+
if (!remembered.has(child)) {
|
|
1053
|
+
add(entry.mode === "deep" ? "history.deep" : "history.shallow", location, `${label} history record "${historyPath}" omits parallel region "${child}"`, child);
|
|
1054
|
+
}
|
|
1055
|
+
else if (recurse) {
|
|
1056
|
+
validateRememberedControl(child, true);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
};
|
|
1061
|
+
validateRememberedControl(historyNode.parent, entry.mode === "deep");
|
|
1062
|
+
}
|
|
1063
|
+
};
|
|
1064
|
+
const isCompletedControl = (path, active) => {
|
|
1065
|
+
if (!active.has(path))
|
|
1066
|
+
return false;
|
|
1067
|
+
const node = byPath.get(path);
|
|
1068
|
+
if (node === undefined)
|
|
1069
|
+
return false;
|
|
1070
|
+
if (node.type === "final")
|
|
1071
|
+
return true;
|
|
1072
|
+
if (node.type === "compound") {
|
|
1073
|
+
const child = node.children.find((candidate) => active.has(candidate));
|
|
1074
|
+
return child !== undefined && byPath.get(child)?.type === "final";
|
|
1075
|
+
}
|
|
1076
|
+
if (node.type === "parallel") {
|
|
1077
|
+
return node.children.length > 0 && node.children.every((child) => isCompletedControl(child, active));
|
|
1078
|
+
}
|
|
1079
|
+
return false;
|
|
1080
|
+
};
|
|
1081
|
+
const completionSchema = (path, active) => {
|
|
1082
|
+
const node = byPath.get(path);
|
|
1083
|
+
if (node === undefined)
|
|
1084
|
+
return undefined;
|
|
1085
|
+
if (node.type === "compound") {
|
|
1086
|
+
const child = node.children.find((candidate) => active.has(candidate));
|
|
1087
|
+
return child === undefined ? undefined : completionSchema(child, active);
|
|
1088
|
+
}
|
|
1089
|
+
return node.output ?? Schema.Void;
|
|
1090
|
+
};
|
|
1091
|
+
const validateCompletions = (snapshot, location, label, settled) => {
|
|
1092
|
+
const result = new Map();
|
|
1093
|
+
if (!selected.has("completion") || snapshot.root === undefined) {
|
|
1094
|
+
return result;
|
|
1095
|
+
}
|
|
1096
|
+
if (hasOwn(snapshot.root, "completed")) {
|
|
1097
|
+
const completed = snapshot.root.completed;
|
|
1098
|
+
if (!Array.isArray(completed)) {
|
|
1099
|
+
add("completion.record", location, `${label} completed metadata must be an array`);
|
|
1100
|
+
}
|
|
1101
|
+
else {
|
|
1102
|
+
for (const unknownEntry of completed) {
|
|
1103
|
+
if (!isRecord(unknownEntry) || typeof unknownEntry.path !== "string") {
|
|
1104
|
+
add("completion.record", location, `${label} contains an invalid completion record`);
|
|
1105
|
+
continue;
|
|
1106
|
+
}
|
|
1107
|
+
const path = unknownEntry.path;
|
|
1108
|
+
if (result.has(path)) {
|
|
1109
|
+
add("completion.record", location, `${label} repeats completion "${path}"`, path);
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
result.set(path, unknownEntry.output);
|
|
1113
|
+
if (!snapshot.active.has(path) || !isCompletedControl(path, snapshot.active)) {
|
|
1114
|
+
add("completion.record", location, `${label} completion "${path}" is not actively complete`, path);
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
const schema = completionSchema(path, snapshot.active);
|
|
1118
|
+
if (!schemaMatches(schema, unknownEntry.output)) {
|
|
1119
|
+
add("completion.output", location, `${label} completion output for "${path}" does not match its schema`, path);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
if (settled) {
|
|
1125
|
+
for (const path of snapshot.paths) {
|
|
1126
|
+
if (isCompletedControl(path, snapshot.active) && !result.has(path)) {
|
|
1127
|
+
add("completion.record", location, `${label} omits settled completion "${path}"`, path);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
return result;
|
|
1132
|
+
};
|
|
1133
|
+
const validateSnapshotMetadata = (snapshot, location, label, settled = false) => {
|
|
1134
|
+
validateHistory(snapshot, location, label);
|
|
1135
|
+
return validateCompletions(snapshot, location, label, settled);
|
|
1136
|
+
};
|
|
1137
|
+
const sameControl = (left, right) => {
|
|
1138
|
+
if (!samePaths(left.paths, right.paths))
|
|
1139
|
+
return false;
|
|
1140
|
+
for (const path of left.paths) {
|
|
1141
|
+
if (!sameValue(left.values.get(path), right.values.get(path)))
|
|
1142
|
+
return false;
|
|
1143
|
+
}
|
|
1144
|
+
return sameValue(left.root?.history, right.root?.history);
|
|
1145
|
+
};
|
|
1146
|
+
const sameActivePaths = (left, right) => left.active.size === right.active.size && Array.from(left.active).every((path) => right.active.has(path));
|
|
1147
|
+
const sortedPaths = (paths, direction) => [...new Set(paths)].sort((left, right) => {
|
|
1148
|
+
const depthDifference = direction === "entry" ? depth(left) - depth(right) : depth(right) - depth(left);
|
|
1149
|
+
if (depthDifference !== 0)
|
|
1150
|
+
return depthDifference;
|
|
1151
|
+
const leftOrder = byPath.get(left)?.order ?? Number.MAX_SAFE_INTEGER;
|
|
1152
|
+
const rightOrder = byPath.get(right)?.order ?? Number.MAX_SAFE_INTEGER;
|
|
1153
|
+
return direction === "entry" ? leftOrder - rightOrder : rightOrder - leftOrder;
|
|
1154
|
+
});
|
|
1155
|
+
const validateTransitionBounds = (transition, location) => {
|
|
1156
|
+
if (!selected.has("targetBounds"))
|
|
1157
|
+
return;
|
|
1158
|
+
const definition = definitions.find((candidate) => candidate.source === transition.source && candidate.reenter === transition.reenter &&
|
|
1159
|
+
sameTrigger(candidate.trigger, transition.trigger));
|
|
1160
|
+
if (definition === undefined) {
|
|
1161
|
+
add("targetBounds.definition", location, `retained transition from "${transition.source}" has no public definition`, transition.source);
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
if (transition.target === undefined || definition.targets.type === "dynamic")
|
|
1165
|
+
return;
|
|
1166
|
+
if (!definition.targets.paths.some((bound) => isDescendantOrSelf(String(transition.target), String(bound)))) {
|
|
1167
|
+
add("targetBounds.target", location, `transition target "${String(transition.target)}" is outside declared bounds ` +
|
|
1168
|
+
`[${definition.targets.paths.join(", ")}]`, String(transition.target));
|
|
1169
|
+
}
|
|
1170
|
+
};
|
|
1171
|
+
const validateMicrostep = (microstep, before, after, location) => {
|
|
1172
|
+
if (selected.has("microsteps")) {
|
|
1173
|
+
const uniqueExit = new Set(microstep.exitPaths);
|
|
1174
|
+
const uniqueEntry = new Set(microstep.entryPaths);
|
|
1175
|
+
const reentering = microstep.transitions.filter((transition) => transition.reenter);
|
|
1176
|
+
const inReentryScope = (path, transition) => {
|
|
1177
|
+
const target = transition.target === undefined ? undefined : byPath.get(String(transition.target));
|
|
1178
|
+
if (target?.type === "history" && target.parent !== undefined && before.active.has(target.parent)) {
|
|
1179
|
+
return isDescendantOrSelf(path, target.parent);
|
|
1180
|
+
}
|
|
1181
|
+
const parent = byPath.get(String(transition.source))?.parent;
|
|
1182
|
+
return parent === undefined || path !== parent && isDescendantOrSelf(path, parent);
|
|
1183
|
+
};
|
|
1184
|
+
const commonLifecycleExplained = (path) => microstep.transitions.some((transition) => {
|
|
1185
|
+
if (transition.reenter) {
|
|
1186
|
+
const target = transition.target === undefined ? undefined : byPath.get(String(transition.target));
|
|
1187
|
+
if (target?.type === "history" && target.parent !== undefined && before.active.has(target.parent)) {
|
|
1188
|
+
// Reentry into an active history owner has a dedicated boundary:
|
|
1189
|
+
// only the owner subtree is exited and entered, regardless of
|
|
1190
|
+
// the ordinary source/resolved-target LCA.
|
|
1191
|
+
return isDescendantOrSelf(path, target.parent);
|
|
1192
|
+
}
|
|
1193
|
+
if (inReentryScope(path, transition))
|
|
1194
|
+
return true;
|
|
1195
|
+
}
|
|
1196
|
+
if (transition.resolvedTarget === undefined)
|
|
1197
|
+
return false;
|
|
1198
|
+
const sourceAncestors = ancestors(String(transition.source));
|
|
1199
|
+
const targetAncestors = ancestors(String(transition.resolvedTarget));
|
|
1200
|
+
let boundary;
|
|
1201
|
+
for (let index = 0; index < Math.min(sourceAncestors.length, targetAncestors.length); index++) {
|
|
1202
|
+
if (sourceAncestors[index] !== targetAncestors[index])
|
|
1203
|
+
break;
|
|
1204
|
+
boundary = sourceAncestors[index];
|
|
1205
|
+
}
|
|
1206
|
+
return boundary === undefined || path !== boundary && isDescendantOrSelf(path, boundary);
|
|
1207
|
+
});
|
|
1208
|
+
if (uniqueExit.size !== microstep.exitPaths.length) {
|
|
1209
|
+
add("microsteps.unique", location, "microstep exit paths contain duplicates");
|
|
1210
|
+
}
|
|
1211
|
+
if (uniqueEntry.size !== microstep.entryPaths.length) {
|
|
1212
|
+
add("microsteps.unique", location, "microstep entry paths contain duplicates");
|
|
1213
|
+
}
|
|
1214
|
+
if (!samePaths(microstep.exitPaths, sortedPaths(microstep.exitPaths, "exit"))) {
|
|
1215
|
+
add("microsteps.order", location, "microstep exit paths are not deepest-first in reverse document order");
|
|
1216
|
+
}
|
|
1217
|
+
if (!samePaths(microstep.entryPaths, sortedPaths(microstep.entryPaths, "entry"))) {
|
|
1218
|
+
add("microsteps.order", location, "microstep entry paths are not parent-first in document order");
|
|
1219
|
+
}
|
|
1220
|
+
for (const path of microstep.exitPaths) {
|
|
1221
|
+
if (!before.active.has(path)) {
|
|
1222
|
+
add("microsteps.activeBefore", location, `microstep exits inactive state "${path}"`, path);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
for (const path of microstep.entryPaths) {
|
|
1226
|
+
if (!after.active.has(path)) {
|
|
1227
|
+
add("microsteps.activeAfter", location, `microstep enters state "${path}" absent from its next state`, path);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
for (const path of before.paths) {
|
|
1231
|
+
if (!after.active.has(path) && !uniqueExit.has(path)) {
|
|
1232
|
+
add("microsteps.activeBefore", location, `removed state "${path}" is missing from exit paths`, path);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
for (const path of after.paths) {
|
|
1236
|
+
if (!before.active.has(path) && !uniqueEntry.has(path)) {
|
|
1237
|
+
add("microsteps.activeAfter", location, `added state "${path}" is missing from entry paths`, path);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
for (const transition of reentering) {
|
|
1241
|
+
for (const path of before.paths) {
|
|
1242
|
+
if (inReentryScope(path, transition) && !uniqueExit.has(path)) {
|
|
1243
|
+
add("microsteps.reentry", location, `reentering transition from "${String(transition.source)}" omits exit lifecycle for "${path}"`, path);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
for (const path of after.paths) {
|
|
1247
|
+
if (inReentryScope(path, transition) && !uniqueEntry.has(path)) {
|
|
1248
|
+
add("microsteps.reentry", location, `reentering transition from "${String(transition.source)}" omits entry lifecycle for "${path}"`, path);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
for (const path of before.paths) {
|
|
1253
|
+
if (!after.active.has(path) || !uniqueExit.has(path) && !uniqueEntry.has(path))
|
|
1254
|
+
continue;
|
|
1255
|
+
if (!commonLifecycleExplained(path)) {
|
|
1256
|
+
add("microsteps.reentry", location, `common state "${path}" has lifecycle without a reentering transition`, path);
|
|
1257
|
+
}
|
|
1258
|
+
else if (!uniqueExit.has(path) || !uniqueEntry.has(path)) {
|
|
1259
|
+
add("microsteps.reentry", location, `reentered common state "${path}" must have both exit and entry lifecycle`, path);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
if (!microstep.changed) {
|
|
1263
|
+
if (microstep.exitPaths.length > 0 || microstep.entryPaths.length > 0) {
|
|
1264
|
+
add("microsteps.changed", location, "unchanged microstep contains entry or exit paths");
|
|
1265
|
+
}
|
|
1266
|
+
if (!sameActivePaths(before, after)) {
|
|
1267
|
+
add("microsteps.changed", location, "unchanged microstep changes its active state paths");
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
else if (microstep.exitPaths.length === 0 && microstep.entryPaths.length === 0 && sameActivePaths(before, after)) {
|
|
1271
|
+
add("microsteps.changed", location, "changed microstep has no control-state change or reentry evidence");
|
|
1272
|
+
}
|
|
1273
|
+
for (const transition of microstep.transitions) {
|
|
1274
|
+
if (!before.active.has(String(transition.source)) &&
|
|
1275
|
+
byPath.get(String(transition.source))?.type !== "choice") {
|
|
1276
|
+
add("microsteps.activeBefore", location, `transition source "${String(transition.source)}" is inactive before the microstep`, String(transition.source));
|
|
1277
|
+
}
|
|
1278
|
+
if (transition.resolvedTarget !== undefined && !after.active.has(String(transition.resolvedTarget)) &&
|
|
1279
|
+
microstep.transitions.length === 1) {
|
|
1280
|
+
add("microsteps.activeAfter", location, `resolved target "${String(transition.resolvedTarget)}" is inactive after the microstep`, String(transition.resolvedTarget));
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
for (const transition of microstep.transitions)
|
|
1285
|
+
validateTransitionBounds(transition, location);
|
|
1286
|
+
};
|
|
1287
|
+
const validatePlanCompletion = (plan, snapshot, completions, location, label) => {
|
|
1288
|
+
if (!selected.has("completion"))
|
|
1289
|
+
return;
|
|
1290
|
+
const rootPath = snapshot.paths.find((path) => byPath.get(path)?.parent === undefined);
|
|
1291
|
+
const hasRootDoneTransition = rootPath !== undefined &&
|
|
1292
|
+
definitions.some((definition) => definition.source === rootPath && definition.trigger.type === "done");
|
|
1293
|
+
const terminal = rootPath !== undefined && isCompletedControl(rootPath, snapshot.active) && !hasRootDoneTransition;
|
|
1294
|
+
if (plan.done !== terminal) {
|
|
1295
|
+
add("completion.done", location, `${label} reports done=${String(plan.done)} for terminal=${String(terminal)}`, rootPath);
|
|
1296
|
+
}
|
|
1297
|
+
if (plan.done) {
|
|
1298
|
+
if (rootPath === undefined || !completions.has(rootPath)) {
|
|
1299
|
+
add("completion.output", location, `${label} done plan has no root completion output`, rootPath);
|
|
1300
|
+
}
|
|
1301
|
+
else if (!sameValue(plan.output, completions.get(rootPath))) {
|
|
1302
|
+
add("completion.output", location, `${label} output differs from its root completion`, rootPath);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
else if (plan.output !== undefined) {
|
|
1306
|
+
add("completion.output", location, `${label} non-done plan exposes an output`, rootPath);
|
|
1307
|
+
}
|
|
1308
|
+
};
|
|
1309
|
+
const initialLocation = { eventIndex: undefined };
|
|
1310
|
+
const starting = inspectSnapshot(trace.initial.startingState, initialLocation, "initial starting state");
|
|
1311
|
+
validateSnapshotMetadata(starting, initialLocation, "initial starting state");
|
|
1312
|
+
validateTraceConfiguration(starting, trace.initial.startingConfiguration, initialLocation, "initial starting");
|
|
1313
|
+
if (selected.has("microsteps")) {
|
|
1314
|
+
if (new Set(trace.initial.initialEntryPaths).size !== trace.initial.initialEntryPaths.length) {
|
|
1315
|
+
add("microsteps.unique", initialLocation, "initial entry paths contain duplicates");
|
|
1316
|
+
}
|
|
1317
|
+
if (!samePaths(trace.initial.initialEntryPaths, starting.paths)) {
|
|
1318
|
+
add("microsteps.order", initialLocation, "initial entry paths do not cover the starting configuration in definition order");
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
let current = starting;
|
|
1322
|
+
for (let index = 0; index < trace.initial.plan.microsteps.length; index++) {
|
|
1323
|
+
const location = { eventIndex: undefined, microstepIndex: index };
|
|
1324
|
+
const microstep = trace.initial.plan.microsteps[index];
|
|
1325
|
+
const next = inspectSnapshot(microstep.next, location, `initial microstep ${index} next state`);
|
|
1326
|
+
validateSnapshotMetadata(next, location, `initial microstep ${index} next state`);
|
|
1327
|
+
validateMicrostep(microstep, current, next, location);
|
|
1328
|
+
current = next;
|
|
1329
|
+
}
|
|
1330
|
+
const initialState = inspectSnapshot(trace.initial.plan.state, initialLocation, "initial plan state");
|
|
1331
|
+
const initialCompletions = validateSnapshotMetadata(initialState, initialLocation, "initial plan state", true);
|
|
1332
|
+
if (selected.has("microsteps") && !sameControl(current, initialState)) {
|
|
1333
|
+
add("microsteps.continuity", initialLocation, "initial plan state does not continue from its final microstep");
|
|
1334
|
+
}
|
|
1335
|
+
validatePlanCompletion(trace.initial.plan, initialState, initialCompletions, initialLocation, "initial plan");
|
|
1336
|
+
validateTraceConfiguration(initialState, trace.initial.configuration, initialLocation, "initial");
|
|
1337
|
+
if (selected.has("microsteps") && !sameValue(trace.initial.startingState, trace.initial.plan.startingState)) {
|
|
1338
|
+
add("microsteps.continuity", initialLocation, "initial trace starting state differs from its plan");
|
|
1339
|
+
}
|
|
1340
|
+
if (selected.has("microsteps") &&
|
|
1341
|
+
!samePaths(trace.initial.initialEntryPaths, trace.initial.plan.initialEntryPaths)) {
|
|
1342
|
+
add("microsteps.continuity", initialLocation, "initial trace entry paths differ from its plan");
|
|
1343
|
+
}
|
|
1344
|
+
let previous = initialState;
|
|
1345
|
+
for (let eventIndex = 0; eventIndex < trace.steps.length; eventIndex++) {
|
|
1346
|
+
const step = trace.steps[eventIndex];
|
|
1347
|
+
const location = { eventIndex };
|
|
1348
|
+
const before = inspectSnapshot(step.before, location, `event ${eventIndex} before state`);
|
|
1349
|
+
validateSnapshotMetadata(before, location, `event ${eventIndex} before state`, true);
|
|
1350
|
+
validateTraceConfiguration(before, step.beforeConfiguration, location, `event ${eventIndex} before`);
|
|
1351
|
+
if (selected.has("microsteps")) {
|
|
1352
|
+
if (step.index !== eventIndex) {
|
|
1353
|
+
add("microsteps.continuity", location, `trace step index ${step.index} does not equal ${eventIndex}`);
|
|
1354
|
+
}
|
|
1355
|
+
if (!sameValue(previous.root, before.root)) {
|
|
1356
|
+
add("microsteps.continuity", location, `event ${eventIndex} before state does not equal the previous state`);
|
|
1357
|
+
}
|
|
1358
|
+
if (!sameValue(step.event, trace.scenario.events[eventIndex])) {
|
|
1359
|
+
add("microsteps.continuity", location, `event ${eventIndex} differs from its scenario event`);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
current = before;
|
|
1363
|
+
for (let microstepIndex = 0; microstepIndex < step.plan.microsteps.length; microstepIndex++) {
|
|
1364
|
+
const microstepLocation = { eventIndex, microstepIndex };
|
|
1365
|
+
const microstep = step.plan.microsteps[microstepIndex];
|
|
1366
|
+
const next = inspectSnapshot(microstep.next, microstepLocation, `event ${eventIndex} microstep ${microstepIndex} next state`);
|
|
1367
|
+
validateSnapshotMetadata(next, microstepLocation, `event ${eventIndex} microstep ${microstepIndex} next state`);
|
|
1368
|
+
validateMicrostep(microstep, current, next, microstepLocation);
|
|
1369
|
+
current = next;
|
|
1370
|
+
}
|
|
1371
|
+
const plannedNext = inspectSnapshot(step.plan.next, location, `event ${eventIndex} plan next state`);
|
|
1372
|
+
const completions = validateSnapshotMetadata(plannedNext, location, `event ${eventIndex} plan next state`, true);
|
|
1373
|
+
if (selected.has("microsteps") && !sameControl(current, plannedNext)) {
|
|
1374
|
+
add("microsteps.continuity", location, `event ${eventIndex} plan next state does not continue its final microstep`);
|
|
1375
|
+
}
|
|
1376
|
+
validatePlanCompletion(step.plan, plannedNext, completions, location, `event ${eventIndex} plan`);
|
|
1377
|
+
const after = inspectSnapshot(step.after, location, `event ${eventIndex} after state`);
|
|
1378
|
+
validateSnapshotMetadata(after, location, `event ${eventIndex} after state`, true);
|
|
1379
|
+
validateTraceConfiguration(after, step.afterConfiguration, location, `event ${eventIndex} after`);
|
|
1380
|
+
if (selected.has("microsteps") && !sameValue(step.plan.next, step.after)) {
|
|
1381
|
+
add("microsteps.continuity", location, `event ${eventIndex} after state differs from its plan next state`);
|
|
1382
|
+
}
|
|
1383
|
+
previous = after;
|
|
1384
|
+
}
|
|
1385
|
+
const finalEventIndex = trace.steps.length === 0 ? undefined : trace.steps.length - 1;
|
|
1386
|
+
const finalLocation = { eventIndex: finalEventIndex };
|
|
1387
|
+
const final = inspectSnapshot(trace.final, finalLocation, "trace final state");
|
|
1388
|
+
validateSnapshotMetadata(final, finalLocation, "trace final state", true);
|
|
1389
|
+
validateTraceConfiguration(final, trace.finalConfiguration, finalLocation, "final");
|
|
1390
|
+
if (selected.has("microsteps") && !sameValue(previous.root, final.root)) {
|
|
1391
|
+
add("microsteps.continuity", finalLocation, "trace final state differs from its final planned state");
|
|
1392
|
+
}
|
|
1393
|
+
return violations.length === 0 ? Effect.void : Effect.fail(new VerificationError({ violations }));
|
|
1394
|
+
};
|
|
1395
|
+
const formatConfiguration = (paths) => `[${paths.join(", ")}]`;
|
|
1396
|
+
const formatMicrosteps = (microsteps) => microsteps.map((microstep, index) => {
|
|
1397
|
+
const transitions = microstep.transitions.map((transition) => ({
|
|
1398
|
+
source: transition.source,
|
|
1399
|
+
trigger: transition.trigger,
|
|
1400
|
+
reenter: transition.reenter,
|
|
1401
|
+
target: transition.target,
|
|
1402
|
+
resolvedTarget: transition.resolvedTarget
|
|
1403
|
+
}));
|
|
1404
|
+
return ` microstep ${index}: event=${formatValue(microstep.event)} changed=${String(microstep.changed)} ` +
|
|
1405
|
+
`transitions=${formatValue(transitions)} exit=${formatConfiguration(microstep.exitPaths)} ` +
|
|
1406
|
+
`entry=${formatConfiguration(microstep.entryPaths)} commands=${microstep.commands.length} ` +
|
|
1407
|
+
`raised=${formatValue(microstep.raisedEvents)} emitted=${formatValue(microstep.emittedEvents)} ` +
|
|
1408
|
+
`next=${formatValue(microstep.next)}`;
|
|
1409
|
+
});
|
|
1410
|
+
const formatInitial = (initial) => [
|
|
1411
|
+
`initial: startingConfiguration=${formatConfiguration(initial.startingConfiguration)} ` +
|
|
1412
|
+
`startingState=${formatValue(initial.startingState)} initialEntry=${formatConfiguration(initial.initialEntryPaths)} ` +
|
|
1413
|
+
`configuration=${formatConfiguration(initial.configuration)} state=${formatValue(initial.plan.state)} ` +
|
|
1414
|
+
`done=${String(initial.plan.done)} output=${formatValue(initial.plan.output)} ` +
|
|
1415
|
+
`commands=${initial.plan.commands.length} emitted=${formatValue(initial.plan.emittedEvents)}`,
|
|
1416
|
+
...formatMicrosteps(initial.plan.microsteps)
|
|
1417
|
+
];
|
|
1418
|
+
const formatStep = (step) => [
|
|
1419
|
+
`step ${step.index}: event=${formatValue(step.event)} before=${formatConfiguration(step.beforeConfiguration)} ` +
|
|
1420
|
+
`after=${formatConfiguration(step.afterConfiguration)} state=${formatValue(step.after)} ` +
|
|
1421
|
+
`done=${String(step.plan.done)} output=${formatValue(step.plan.output)} ` +
|
|
1422
|
+
`commands=${step.plan.commands.length} emitted=${formatValue(step.plan.emittedEvents)}`,
|
|
1423
|
+
...formatMicrosteps(step.plan.microsteps)
|
|
1424
|
+
];
|
|
1425
|
+
const isRunFailure = (trace) => "_tag" in trace && trace._tag === "MachineTestRunFailure";
|
|
1426
|
+
export const formatTrace = (trace) => {
|
|
1427
|
+
const lines = [`scenario: ${formatValue(trace.scenario)}`];
|
|
1428
|
+
if (isRunFailure(trace)) {
|
|
1429
|
+
if (trace.initial !== undefined) {
|
|
1430
|
+
lines.push(...formatInitial(trace.initial));
|
|
1431
|
+
for (const step of trace.steps) {
|
|
1432
|
+
lines.push(...formatStep(step));
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
lines.push(`failure: phase=${trace.phase} eventIndex=${formatValue(trace.eventIndex)} ` +
|
|
1436
|
+
`event=${formatValue(trace.event)} cause=${formatValue(trace.cause)}`);
|
|
1437
|
+
return lines.join("\n");
|
|
1438
|
+
}
|
|
1439
|
+
lines.push(...formatInitial(trace.initial));
|
|
1440
|
+
for (const step of trace.steps) {
|
|
1441
|
+
lines.push(...formatStep(step));
|
|
1442
|
+
}
|
|
1443
|
+
lines.push(`final: configuration=${formatConfiguration(trace.finalConfiguration)} state=${formatValue(trace.final)}`);
|
|
1444
|
+
return lines.join("\n");
|
|
1445
|
+
};
|
|
1446
|
+
//# sourceMappingURL=verification.js.map
|