@typeonce/effect-machine 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +4 -3
  2. package/dist/Machine.d.ts +24 -6
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js.map +1 -1
  5. package/dist/internal/machine/atom.d.ts +5 -0
  6. package/dist/internal/machine/atom.d.ts.map +1 -1
  7. package/dist/internal/machine/atom.js +6 -3
  8. package/dist/internal/machine/atom.js.map +1 -1
  9. package/dist/internal/machine/machine.d.ts.map +1 -1
  10. package/dist/internal/machine/machine.js +1 -1
  11. package/dist/internal/machine/machine.js.map +1 -1
  12. package/dist/unstable/reactivity/AtomMachine.d.ts +26 -0
  13. package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
  14. package/dist/unstable/reactivity/AtomMachine.js +23 -0
  15. package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
  16. package/docs/agent-guide.md +14 -0
  17. package/package.json +5 -5
  18. package/src/Machine.ts +6909 -0
  19. package/src/index.ts +1 -0
  20. package/src/internal/machine/activities.ts +108 -0
  21. package/src/internal/machine/atom.ts +684 -0
  22. package/src/internal/machine/cluster.ts +394 -0
  23. package/src/internal/machine/command.ts +58 -0
  24. package/src/internal/machine/commandRuntime.ts +43 -0
  25. package/src/internal/machine/configuration.ts +1331 -0
  26. package/src/internal/machine/errors.ts +87 -0
  27. package/src/internal/machine/executionPlan.ts +996 -0
  28. package/src/internal/machine/invocation.ts +119 -0
  29. package/src/internal/machine/machine.ts +1750 -0
  30. package/src/internal/machine/planner.ts +1933 -0
  31. package/src/internal/machine/process.ts +906 -0
  32. package/src/internal/machine/protocol.ts +322 -0
  33. package/src/internal/machine/readiness.ts +10 -0
  34. package/src/internal/machine/runtime.ts +2512 -0
  35. package/src/internal/machine/serialization.ts +498 -0
  36. package/src/internal/machine/stateDefinition.ts +270 -0
  37. package/src/internal/machine/symbols.ts +2 -0
  38. package/src/internal/machine/topology.ts +479 -0
  39. package/src/internal/testing/machine/arbitrary.ts +102 -0
  40. package/src/internal/testing/machine/exploration.ts +331 -0
  41. package/src/internal/testing/machine/finiteModel.ts +1498 -0
  42. package/src/internal/testing/machine/invariant.ts +372 -0
  43. package/src/internal/testing/machine/probe.ts +79 -0
  44. package/src/internal/testing/machine/referenceModel.ts +1505 -0
  45. package/src/internal/testing/machine/runtime.ts +1710 -0
  46. package/src/internal/testing/machine/runtimeInvariant.ts +486 -0
  47. package/src/internal/testing/machine/trace.ts +150 -0
  48. package/src/internal/testing/machine/verification.ts +1890 -0
  49. package/src/testing/MachineTest.ts +2067 -0
  50. package/src/testing/index.ts +7 -0
  51. package/src/unstable/cluster/ClusterMachine.ts +390 -0
  52. package/src/unstable/cluster/index.ts +1 -0
  53. package/src/unstable/reactivity/AtomMachine.ts +696 -0
  54. package/src/unstable/reactivity/index.ts +1 -0
@@ -0,0 +1,486 @@
1
+ /**
2
+ * User-defined semantic invariants over retained causal runtime evidence.
3
+ *
4
+ * @since 0.4.0
5
+ */
6
+
7
+ import * as Data from "effect/Data"
8
+ import * as Effect from "effect/Effect"
9
+ import * as Result from "effect/Result"
10
+ import * as Machine from "../../../Machine.js"
11
+ import type {
12
+ CausalRuntimeEvidence,
13
+ InvariantOptions,
14
+ InvariantOutcome,
15
+ PlannerRuntimeAgreementViolation,
16
+ RuntimeCommandInvariant,
17
+ RuntimeCommandInvariantContext,
18
+ RuntimeInvariant,
19
+ RuntimeInvariantBuilder,
20
+ RuntimeInvariantCheckResult,
21
+ RuntimeInvariantRecord,
22
+ RuntimeInvariantReport,
23
+ RuntimeInvariantScope,
24
+ RuntimeInvariantTranscript,
25
+ RuntimeInvariantViolation,
26
+ RuntimeSnapshotInvariant,
27
+ RuntimeSnapshotInvariantContext,
28
+ RuntimeSnapshotInvariantOptions,
29
+ RuntimeSnapshotObservation,
30
+ RuntimeTranscriptInvariant,
31
+ RuntimeTranscriptInvariantContext
32
+ } from "../../../testing/MachineTest.js"
33
+
34
+ type AnyMachine = Machine.Machine.Any
35
+
36
+ const validateName = (name: string): void => {
37
+ if (name.trim().length === 0) {
38
+ throw new Error("MachineTest.runtimeInvariants expected name to be a non-empty string")
39
+ }
40
+ }
41
+
42
+ const validateRequirement = <Context>(options: InvariantOptions<Context>): void => {
43
+ const minimum = options.require?.minObservations
44
+ if (minimum !== undefined && (!Number.isSafeInteger(minimum) || minimum < 0)) {
45
+ throw new Error("MachineTest.runtimeInvariants expected minObservations to be a non-negative safe integer")
46
+ }
47
+ }
48
+
49
+ const snapshotInvariant = <M extends AnyMachine>(
50
+ name: string,
51
+ check: (context: RuntimeSnapshotInvariantContext<M>) => InvariantOutcome,
52
+ options: RuntimeSnapshotInvariantOptions<M> = {}
53
+ ): RuntimeSnapshotInvariant<M> => {
54
+ validateName(name)
55
+ validateRequirement(options)
56
+ return Object.freeze({
57
+ _tag: "RuntimeSnapshotInvariant" as const,
58
+ name,
59
+ observe: options.observe ?? "settled",
60
+ check,
61
+ ...(options.when === undefined ? {} : { when: options.when }),
62
+ ...(options.require === undefined ? {} : { require: Object.freeze({ ...options.require }) })
63
+ })
64
+ }
65
+
66
+ const commandInvariant = <M extends AnyMachine>(
67
+ name: string,
68
+ check: (context: RuntimeCommandInvariantContext<M>) => InvariantOutcome,
69
+ options: InvariantOptions<RuntimeCommandInvariantContext<M>> = {}
70
+ ): RuntimeCommandInvariant<M> => {
71
+ validateName(name)
72
+ validateRequirement(options)
73
+ return Object.freeze({
74
+ _tag: "RuntimeCommandInvariant" as const,
75
+ name,
76
+ check,
77
+ ...(options.when === undefined ? {} : { when: options.when }),
78
+ ...(options.require === undefined ? {} : { require: Object.freeze({ ...options.require }) })
79
+ })
80
+ }
81
+
82
+ const transcriptInvariant = <M extends AnyMachine>(
83
+ name: string,
84
+ check: (context: RuntimeTranscriptInvariantContext<M>) => InvariantOutcome,
85
+ options: InvariantOptions<RuntimeTranscriptInvariantContext<M>> = {}
86
+ ): RuntimeTranscriptInvariant<M> => {
87
+ validateName(name)
88
+ validateRequirement(options)
89
+ return Object.freeze({
90
+ _tag: "RuntimeTranscriptInvariant" as const,
91
+ name,
92
+ check,
93
+ ...(options.when === undefined ? {} : { when: options.when }),
94
+ ...(options.require === undefined ? {} : { require: Object.freeze({ ...options.require }) })
95
+ })
96
+ }
97
+
98
+ export const runtimeInvariants = <M extends AnyMachine>(_machine: M): RuntimeInvariantBuilder<M> =>
99
+ Object.freeze({
100
+ snapshot: snapshotInvariant,
101
+ command: commandInvariant,
102
+ transcript: transcriptInvariant
103
+ })
104
+
105
+ export class RuntimeInvariantError<M extends AnyMachine = AnyMachine> extends Data.TaggedError(
106
+ "MachineTestRuntimeInvariantError"
107
+ )<{
108
+ readonly transcript: RuntimeInvariantTranscript<M>
109
+ readonly violations: ReadonlyArray<RuntimeInvariantViolation<M>>
110
+ readonly report: RuntimeInvariantReport
111
+ }> {}
112
+
113
+ const normalizeTranscript = <M extends AnyMachine, Error, Output>(
114
+ transcript: CausalRuntimeEvidence<M, Error, Output>
115
+ ): RuntimeInvariantTranscript<M> => ({
116
+ commands: transcript.commands,
117
+ initial: transcript.initial as RuntimeInvariantTranscript<M>["initial"],
118
+ records: transcript.records.map(({ actual, command, index }) => ({
119
+ index,
120
+ command,
121
+ result: actual.result,
122
+ snapshot: actual.snapshot as RuntimeInvariantRecord<M>["snapshot"],
123
+ awaited: actual.awaited as RuntimeInvariantRecord<M>["awaited"]
124
+ })),
125
+ final: transcript.final as RuntimeInvariantTranscript<M>["final"]
126
+ })
127
+
128
+ interface Observation<M extends AnyMachine, Context> {
129
+ readonly context: Context
130
+ readonly observationIndex?: number
131
+ readonly commandIndex: number | undefined
132
+ readonly awaitedIndex?: number
133
+ readonly phase?: RuntimeSnapshotObservation
134
+ readonly command?: RuntimeInvariantRecord<M>["command"]
135
+ }
136
+
137
+ const snapshotObservations = <M extends AnyMachine>(
138
+ machine: M,
139
+ transcript: RuntimeInvariantTranscript<M>,
140
+ invariant: RuntimeSnapshotInvariant<M>
141
+ ): ReadonlyArray<Observation<M, RuntimeSnapshotInvariantContext<M>>> => {
142
+ const observations: Array<Observation<M, RuntimeSnapshotInvariantContext<M>>> = []
143
+ let observationIndex = 0
144
+ const add = (
145
+ snapshot: RuntimeInvariantTranscript<M>["initial"],
146
+ phase: RuntimeSnapshotObservation,
147
+ record?: RuntimeInvariantRecord<M>,
148
+ awaitedIndex?: number
149
+ ): void => {
150
+ const context: RuntimeSnapshotInvariantContext<M> = {
151
+ machine,
152
+ transcript,
153
+ snapshot,
154
+ observationIndex,
155
+ phase,
156
+ commandIndex: record?.index,
157
+ awaitedIndex,
158
+ command: record?.command,
159
+ result: record?.result
160
+ }
161
+ observations.push({
162
+ context,
163
+ observationIndex,
164
+ commandIndex: record?.index,
165
+ ...(awaitedIndex === undefined ? {} : { awaitedIndex }),
166
+ phase,
167
+ ...(record === undefined ? {} : { command: record.command })
168
+ })
169
+ observationIndex += 1
170
+ }
171
+
172
+ if (invariant.observe === "final") {
173
+ add(transcript.final, "final")
174
+ return observations
175
+ }
176
+
177
+ if (invariant.observe === "settled" || invariant.observe === "all") {
178
+ add(transcript.initial, "initial")
179
+ for (const record of transcript.records) add(record.snapshot, "command", record)
180
+ }
181
+
182
+ if (invariant.observe === "awaited" || invariant.observe === "all") {
183
+ for (const record of transcript.records) {
184
+ record.awaited.forEach((snapshot, awaitedIndex) => add(snapshot, "awaited", record, awaitedIndex))
185
+ }
186
+ }
187
+
188
+ return observations
189
+ }
190
+
191
+ const commandObservations = <M extends AnyMachine>(
192
+ machine: M,
193
+ transcript: RuntimeInvariantTranscript<M>
194
+ ): ReadonlyArray<Observation<M, RuntimeCommandInvariantContext<M>>> =>
195
+ transcript.records.map((record, index) => ({
196
+ context: {
197
+ machine,
198
+ transcript,
199
+ record,
200
+ previous: transcript.records[index - 1],
201
+ index: record.index,
202
+ command: record.command,
203
+ result: record.result,
204
+ snapshot: record.snapshot,
205
+ awaited: record.awaited
206
+ },
207
+ commandIndex: record.index,
208
+ command: record.command
209
+ }))
210
+
211
+ const transcriptObservations = <M extends AnyMachine>(
212
+ machine: M,
213
+ transcript: RuntimeInvariantTranscript<M>
214
+ ): ReadonlyArray<Observation<M, RuntimeTranscriptInvariantContext<M>>> => [{
215
+ context: { machine, transcript },
216
+ commandIndex: undefined
217
+ }]
218
+
219
+ const scopeOf = <M extends AnyMachine>(invariant: RuntimeInvariant<M>): RuntimeInvariantScope => {
220
+ switch (invariant._tag) {
221
+ case "RuntimeSnapshotInvariant":
222
+ return "snapshot"
223
+ case "RuntimeCommandInvariant":
224
+ return "command"
225
+ case "RuntimeTranscriptInvariant":
226
+ return "transcript"
227
+ }
228
+ }
229
+
230
+ const messageOf = (outcome: InvariantOutcome): string | undefined =>
231
+ outcome === true ? undefined : typeof outcome === "string" ? outcome : "Invariant predicate returned false"
232
+
233
+ type EvaluatedInvariant<Context> = InvariantOptions<Context> & {
234
+ readonly name: string
235
+ readonly check: (context: Context) => InvariantOutcome
236
+ }
237
+
238
+ const evaluateInvariant = <M extends AnyMachine, Context>(
239
+ invariant: EvaluatedInvariant<Context>,
240
+ scope: RuntimeInvariantScope,
241
+ observations: ReadonlyArray<Observation<M, Context>>
242
+ ): {
243
+ readonly check: RuntimeInvariantCheckResult
244
+ readonly violations: ReadonlyArray<RuntimeInvariantViolation<M>>
245
+ } => {
246
+ let observed = 0
247
+ let failures = 0
248
+ const violations: Array<RuntimeInvariantViolation<M>> = []
249
+
250
+ for (const observation of observations) {
251
+ if (invariant.when !== undefined && !invariant.when(observation.context)) continue
252
+ observed += 1
253
+ const message = messageOf(invariant.check(observation.context))
254
+ if (message === undefined) continue
255
+ failures += 1
256
+ violations.push({
257
+ invariant: invariant.name,
258
+ scope,
259
+ kind: "predicate",
260
+ ...(observation.observationIndex === undefined ? {} : { observationIndex: observation.observationIndex }),
261
+ commandIndex: observation.commandIndex,
262
+ ...(observation.awaitedIndex === undefined ? {} : { awaitedIndex: observation.awaitedIndex }),
263
+ ...(observation.phase === undefined ? {} : { phase: observation.phase }),
264
+ ...(observation.command === undefined ? {} : { command: observation.command }),
265
+ message
266
+ })
267
+ }
268
+
269
+ const minimum = invariant.require?.minObservations ?? 0
270
+ const insufficient = observed < minimum
271
+ if (insufficient) {
272
+ violations.push({
273
+ invariant: invariant.name,
274
+ scope,
275
+ kind: "observations",
276
+ commandIndex: undefined,
277
+ message: `Invariant required at least ${minimum} observation${minimum === 1 ? "" : "s"} but observed ${observed}`
278
+ })
279
+ }
280
+
281
+ return {
282
+ check: {
283
+ invariant: invariant.name,
284
+ scope,
285
+ status: failures > 0 ? "failed" : insufficient ? "insufficient" : observed === 0 ? "untested" : "passed",
286
+ observations: observed,
287
+ failures
288
+ },
289
+ violations
290
+ }
291
+ }
292
+
293
+ export const checkRuntimeInvariants = <M extends AnyMachine, Error, Output>(
294
+ machine: M,
295
+ evidence: CausalRuntimeEvidence<M, Error, Output>,
296
+ invariants: ReadonlyArray<RuntimeInvariant<M>>
297
+ ): Effect.Effect<RuntimeInvariantReport, RuntimeInvariantError<M>> =>
298
+ Effect.suspend(() => {
299
+ const transcript = normalizeTranscript(evidence)
300
+ const checks: Array<RuntimeInvariantCheckResult> = []
301
+ const violations: Array<RuntimeInvariantViolation<M>> = []
302
+
303
+ for (const invariant of invariants) {
304
+ const scope = scopeOf(invariant)
305
+ const result = invariant._tag === "RuntimeSnapshotInvariant"
306
+ ? evaluateInvariant(invariant, scope, snapshotObservations(machine, transcript, invariant))
307
+ : invariant._tag === "RuntimeCommandInvariant"
308
+ ? evaluateInvariant(invariant, scope, commandObservations(machine, transcript))
309
+ : evaluateInvariant(invariant, scope, transcriptObservations(machine, transcript))
310
+ checks.push(result.check)
311
+ violations.push(...result.violations)
312
+ }
313
+
314
+ const report: RuntimeInvariantReport = { checks }
315
+ return violations.length === 0
316
+ ? Effect.succeed(report)
317
+ : Effect.fail(new RuntimeInvariantError({ transcript, violations, report }))
318
+ })
319
+
320
+ export const assertRuntimeInvariants = <M extends AnyMachine, Error, Output>(
321
+ machine: M,
322
+ transcript: CausalRuntimeEvidence<M, Error, Output>,
323
+ invariants: ReadonlyArray<RuntimeInvariant<M>>
324
+ ): Effect.Effect<void, RuntimeInvariantError<M>> =>
325
+ checkRuntimeInvariants(machine, transcript, invariants).pipe(Effect.asVoid)
326
+
327
+ export class PlannerRuntimeAgreementError<M extends AnyMachine = AnyMachine> extends Data.TaggedError(
328
+ "MachineTestPlannerRuntimeAgreementError"
329
+ )<{
330
+ readonly evidence: CausalRuntimeEvidence<M, unknown, unknown>
331
+ readonly violations: ReadonlyArray<PlannerRuntimeAgreementViolation<M>>
332
+ }> {}
333
+
334
+ const fingerprint = (value: unknown): string => {
335
+ const active = new WeakSet<object>()
336
+ const visit = (current: unknown): unknown => {
337
+ if (current === undefined) return ["undefined"]
338
+ if (current === null || typeof current === "boolean" || typeof current === "string") return current
339
+ if (typeof current === "number") {
340
+ if (Number.isNaN(current)) return ["number", "NaN"]
341
+ if (Object.is(current, -0)) return ["number", "-0"]
342
+ return current
343
+ }
344
+ if (typeof current === "bigint") return ["bigint", String(current)]
345
+ if (typeof current === "symbol") return ["symbol", Symbol.keyFor(current) ?? String(current)]
346
+ if (typeof current === "function") return ["function", current.name]
347
+ if (active.has(current)) return ["circular"]
348
+ active.add(current)
349
+ let result: unknown
350
+ if (current instanceof Date) {
351
+ result = ["date", Number.isNaN(current.getTime()) ? "Invalid Date" : current.toISOString()]
352
+ } else if (current instanceof Error) {
353
+ result = [
354
+ "error",
355
+ current.name,
356
+ current.message,
357
+ Object.keys(current).sort().map((key) => [key, visit((current as unknown as Record<string, unknown>)[key])])
358
+ ]
359
+ } else if (ArrayBuffer.isView(current)) {
360
+ result = [current.constructor.name, Array.from(current as unknown as ArrayLike<number>)]
361
+ } else if (Array.isArray(current)) {
362
+ result = current.map(visit)
363
+ } else if (current instanceof Map) {
364
+ result = [
365
+ "map",
366
+ Array.from(current, ([key, item]) => [visit(key), visit(item)]).sort((left, right) =>
367
+ JSON.stringify(left).localeCompare(JSON.stringify(right))
368
+ )
369
+ ]
370
+ } else if (current instanceof Set) {
371
+ result = [
372
+ "set",
373
+ Array.from(current, visit).sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)))
374
+ ]
375
+ } else {
376
+ result = Object.keys(current).sort().map((key) => [key, visit((current as Record<string, unknown>)[key])])
377
+ }
378
+ active.delete(current)
379
+ return result
380
+ }
381
+ return JSON.stringify(visit(value))
382
+ }
383
+
384
+ const projection = (plan: {
385
+ readonly next: unknown
386
+ readonly commands: ReadonlyArray<unknown>
387
+ readonly emittedEvents: ReadonlyArray<unknown>
388
+ readonly microsteps: ReadonlyArray<{
389
+ readonly next: unknown
390
+ readonly event: unknown
391
+ readonly commands: ReadonlyArray<unknown>
392
+ readonly raisedEvents: ReadonlyArray<unknown>
393
+ readonly emittedEvents: ReadonlyArray<unknown>
394
+ readonly exitPaths: ReadonlyArray<string>
395
+ readonly entryPaths: ReadonlyArray<string>
396
+ readonly changed: boolean
397
+ }>
398
+ readonly done: boolean
399
+ readonly output: unknown
400
+ }) => ({
401
+ after: plan.next,
402
+ completion: { done: plan.done, output: plan.output },
403
+ commands: plan.commands.length,
404
+ emittedEvents: plan.emittedEvents,
405
+ microsteps: plan.microsteps.map((microstep) => ({
406
+ next: microstep.next,
407
+ event: microstep.event,
408
+ commands: microstep.commands.length,
409
+ raisedEvents: microstep.raisedEvents,
410
+ emittedEvents: microstep.emittedEvents,
411
+ exitPaths: microstep.exitPaths,
412
+ entryPaths: microstep.entryPaths,
413
+ changed: microstep.changed
414
+ }))
415
+ })
416
+
417
+ export const assertPlannerRuntimeAgreement = <M extends AnyMachine, Error, Output>(
418
+ machine: M,
419
+ evidence: CausalRuntimeEvidence<M, Error, Output>
420
+ ): Effect.Effect<void, PlannerRuntimeAgreementError<M>> =>
421
+ Effect.gen(function*() {
422
+ const violations: Array<PlannerRuntimeAgreementViolation<M>> = []
423
+ const add = (
424
+ record: CausalRuntimeEvidence<M, Error, Output>["records"][number],
425
+ field: PlannerRuntimeAgreementViolation<M>["field"],
426
+ message: string
427
+ ): void => {
428
+ violations.push({ commandIndex: record.index, command: record.command, field, message })
429
+ }
430
+
431
+ for (const record of evidence.records) {
432
+ if (record.command._tag !== "Send" || record.actual.result._tag !== "SendProcessed") continue
433
+ const step = record.actual.result.step
434
+ const planned = yield* Effect.result(
435
+ Machine.plan(machine as any, step.before as any, record.command.event as any) as Effect.Effect<any, unknown>
436
+ )
437
+ if (Result.isFailure(planned)) {
438
+ add(record, "planning", `Fresh planning failed: ${String(planned.failure)}`)
439
+ continue
440
+ }
441
+
442
+ const expected = projection(planned.success)
443
+ const actual = projection(step.plan)
444
+ const compare = (
445
+ field: "planNext" | "after" | "completion" | "commands" | "emittedEvents" | "microsteps",
446
+ left: unknown,
447
+ right: unknown
448
+ ): void => {
449
+ const actualFingerprint = fingerprint(left)
450
+ const expectedFingerprint = fingerprint(right)
451
+ if (actualFingerprint !== expectedFingerprint) {
452
+ add(
453
+ record,
454
+ field,
455
+ `Runtime ${field} disagreed with fresh planning: expected ${expectedFingerprint}, actual ${actualFingerprint}`
456
+ )
457
+ }
458
+ }
459
+ compare("planNext", actual.after, expected.after)
460
+ compare("after", step.after, expected.after)
461
+ compare("completion", actual.completion, expected.completion)
462
+ compare("commands", actual.commands, expected.commands)
463
+ compare("emittedEvents", actual.emittedEvents, expected.emittedEvents)
464
+ compare("microsteps", actual.microsteps, expected.microsteps)
465
+
466
+ const handled = planned.success.microsteps.length > 0
467
+ if (step.handled !== handled) add(record, "handled", `Expected handled=${handled} but observed ${step.handled}`)
468
+ const configurationChanged = planned.success.microsteps.some(({ changed }: { readonly changed: boolean }) =>
469
+ changed
470
+ )
471
+ if (step.configurationChanged !== configurationChanged) {
472
+ add(
473
+ record,
474
+ "configurationChanged",
475
+ `Expected configurationChanged=${configurationChanged} but observed ${step.configurationChanged}`
476
+ )
477
+ }
478
+ }
479
+
480
+ if (violations.length > 0) {
481
+ return yield* new PlannerRuntimeAgreementError({
482
+ evidence: evidence as CausalRuntimeEvidence<M, unknown, unknown>,
483
+ violations
484
+ })
485
+ }
486
+ })
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Planner trace construction shared by scenario runs and graph exploration.
3
+ *
4
+ * @since 0.4.0
5
+ */
6
+
7
+ import * as Effect from "effect/Effect"
8
+ import * as Schema from "effect/Schema"
9
+ import * as Machine from "../../../Machine.js"
10
+ import type {
11
+ EventPlan,
12
+ InitialPlan,
13
+ InitialTrace,
14
+ RunError,
15
+ RunFailure,
16
+ RunServices,
17
+ Scenario,
18
+ Trace,
19
+ TraceStep
20
+ } from "../../../testing/MachineTest.js"
21
+ import type { EnsureExecutable } from "../../machine/readiness.js"
22
+
23
+ type AnyMachine = Machine.Machine.Any
24
+
25
+ type InputValue<M extends AnyMachine> = Machine.Machine.Input<M>["Type"]
26
+
27
+ type StatePath<M extends AnyMachine> = Machine.Machine.StateIdentifier<Machine.Machine.States<M>>
28
+
29
+ type ReadyMachine<M extends AnyMachine> =
30
+ & M
31
+ & EnsureExecutable<
32
+ Machine.Machine.States<M>,
33
+ Machine.Machine.UnhandledStates<M>,
34
+ Machine.Machine.OutputStates<M>
35
+ >
36
+
37
+ export const rawConfigurationPaths = <M extends AnyMachine>(
38
+ machine: M,
39
+ snapshot: Machine.Machine.Snapshot<Machine.Machine.States<M>>
40
+ ): ReadonlyArray<StatePath<M>> => {
41
+ const active = new Set<string>()
42
+ const visit = (current: unknown): void => {
43
+ if (typeof current !== "object" || current === null) return
44
+ const value = current as Record<string, unknown>
45
+ if (typeof value.path === "string") active.add(value.path)
46
+ if (value.state !== undefined) visit(value.state)
47
+ if (typeof value.states === "object" && value.states !== null) {
48
+ for (const child of Object.values(value.states)) visit(child)
49
+ }
50
+ }
51
+ visit(snapshot)
52
+ return Machine.stateNodes(machine)
53
+ .filter((node) => node.type !== "history" && node.type !== "choice" && active.has(node.path))
54
+ .map((node) => node.path) as ReadonlyArray<StatePath<M>>
55
+ }
56
+
57
+ export const appendTrace = <M extends AnyMachine>(
58
+ machine: ReadyMachine<M>,
59
+ trace: Trace<M>,
60
+ event: Machine.Machine.InputEvent<M>,
61
+ scenario: Scenario<M>
62
+ ): Effect.Effect<Trace<M>, RunFailure<RunError<M>, M>, RunServices<M>> => {
63
+ const index = trace.steps.length
64
+ const before = trace.final
65
+ const beforeConfiguration = rawConfigurationPaths(machine, before)
66
+ return ((Machine.plan as any)(machine, before, event) as Effect.Effect<
67
+ EventPlan<M>,
68
+ RunError<M>,
69
+ RunServices<M>
70
+ >).pipe(
71
+ Effect.mapError((cause): RunFailure<RunError<M>, M> => ({
72
+ _tag: "MachineTestRunFailure",
73
+ scenario,
74
+ phase: "event",
75
+ eventIndex: index,
76
+ event,
77
+ initial: trace.initial,
78
+ steps: trace.steps.slice(),
79
+ cause
80
+ })),
81
+ Effect.map((plan) => {
82
+ const step: TraceStep<M> = {
83
+ index,
84
+ before,
85
+ beforeConfiguration,
86
+ event,
87
+ plan,
88
+ after: plan.next,
89
+ afterConfiguration: rawConfigurationPaths(machine, plan.next)
90
+ }
91
+ return {
92
+ scenario,
93
+ initial: trace.initial,
94
+ steps: [...trace.steps, step],
95
+ final: plan.next,
96
+ finalConfiguration: step.afterConfiguration
97
+ }
98
+ })
99
+ )
100
+ }
101
+
102
+ export const run: <M extends AnyMachine>(
103
+ machine: ReadyMachine<M>,
104
+ scenario: Scenario<M>
105
+ ) => Effect.Effect<Trace<M>, RunFailure<RunError<M>, M>, RunServices<M>> = Effect.fnUntraced(function*<
106
+ M extends AnyMachine
107
+ >(
108
+ machine: ReadyMachine<M>,
109
+ scenario: Scenario<M>
110
+ ) {
111
+ const initialEffect = (machine.input === undefined || machine.input === Schema.Void
112
+ ? (Machine.planInitial as any)(machine)
113
+ : (Machine.planInitial as any)(machine, (scenario as { readonly input: InputValue<M> }).input)) as Effect.Effect<
114
+ InitialPlan<M>,
115
+ RunError<M>,
116
+ RunServices<M>
117
+ >
118
+ const initial = yield* initialEffect.pipe(
119
+ Effect.mapError((cause): RunFailure<RunError<M>, M> => ({
120
+ _tag: "MachineTestRunFailure",
121
+ scenario,
122
+ phase: "initial",
123
+ eventIndex: undefined,
124
+ event: undefined,
125
+ initial: undefined,
126
+ steps: [],
127
+ cause
128
+ }))
129
+ )
130
+ const initialTrace: InitialTrace<M> = {
131
+ plan: initial,
132
+ startingState: initial.startingState,
133
+ startingConfiguration: rawConfigurationPaths(machine, initial.startingState),
134
+ initialEntryPaths: initial.initialEntryPaths,
135
+ configuration: rawConfigurationPaths(machine, initial.state)
136
+ }
137
+ let trace: Trace<M> = {
138
+ scenario,
139
+ initial: initialTrace,
140
+ steps: [],
141
+ final: initial.state,
142
+ finalConfiguration: initialTrace.configuration
143
+ }
144
+
145
+ for (const event of scenario.events) {
146
+ trace = yield* appendTrace(machine, trace, event, scenario)
147
+ }
148
+
149
+ return trace
150
+ })