@mantaq/viz 0.0.0 → 0.1.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.
@@ -0,0 +1,759 @@
1
+ import { Either } from "@mantaq/utils";
2
+ // src/schema.ts
3
+ /**
4
+ * Viz document schema. Bump `VIZ_SCHEMA_VERSION` on any breaking change.
5
+ * Everything not observable at runtime is `null`, never guessed.
6
+ */
7
+ const VIZ_SCHEMA_VERSION = 7;
8
+ /**
9
+ * Edge source of an `onAny` registration: any non-final state of the machine,
10
+ * or `region.*` inside a region. Never listed in `states`.
11
+ */
12
+ const ANY_STATE = "*";
13
+ /** Region prefix of a state id (`""` at the root). */
14
+ function scopeOf(id) {
15
+ const cut = id.lastIndexOf(".");
16
+ return cut === -1 ? "" : id.slice(0, cut);
17
+ }
18
+ function isAnyState(id) {
19
+ return id === "*" || id.endsWith(`.*`);
20
+ }
21
+ /** True when `stateId` is one of the states an `anyId` source stands for. */
22
+ function coveredByAny(anyId, stateId) {
23
+ return isAnyState(anyId) && !isAnyState(stateId) && scopeOf(anyId) === scopeOf(stateId);
24
+ }
25
+ // src/validate-read.ts
26
+ function isRecord(value) {
27
+ return typeof value === "object" && value !== null && !Array.isArray(value);
28
+ }
29
+ function report(reader, issue) {
30
+ reader.issues.push(issue);
31
+ }
32
+ function fail(reader, at, context) {
33
+ report(reader, {
34
+ path: `${at.path}.${at.key}`,
35
+ message: context.message
36
+ });
37
+ return context.fallback;
38
+ }
39
+ function readString(reader, at) {
40
+ const value = at.record[at.key];
41
+ return typeof value === "string" ? value : fail(reader, at, {
42
+ message: "expected string",
43
+ fallback: ""
44
+ });
45
+ }
46
+ function readNullableString(reader, at) {
47
+ return at.record[at.key] === null ? null : readString(reader, at);
48
+ }
49
+ function readBoolean(reader, at) {
50
+ const value = at.record[at.key];
51
+ return typeof value === "boolean" ? value : fail(reader, at, {
52
+ message: "expected boolean",
53
+ fallback: false
54
+ });
55
+ }
56
+ function readInteger(reader, at) {
57
+ const value = at.record[at.key];
58
+ return typeof value === "number" && Number.isInteger(value) ? value : fail(reader, at, {
59
+ message: "expected integer",
60
+ fallback: 0
61
+ });
62
+ }
63
+ function readStrings(reader, at) {
64
+ const value = at.record[at.key];
65
+ if (Array.isArray(value) && value.every((item) => typeof item === "string")) return value.filter((item) => typeof item === "string");
66
+ return fail(reader, at, {
67
+ message: "expected string[]",
68
+ fallback: []
69
+ });
70
+ }
71
+ function readNullableStrings(reader, at) {
72
+ return at.record[at.key] === null ? null : readStrings(reader, at);
73
+ }
74
+ function readEnum(reader, at, allowed) {
75
+ const value = at.record[at.key];
76
+ const match = allowed.find((candidate) => candidate === value);
77
+ if (match !== void 0) return match;
78
+ return fail(reader, at, {
79
+ message: `expected one of ${allowed.join(", ")}`,
80
+ fallback: void 0
81
+ });
82
+ }
83
+ function readCoverage(reader, at) {
84
+ const value = at.record[at.key];
85
+ if (value === null) return null;
86
+ const hits = isRecord(value) ? value["hits"] : void 0;
87
+ if (typeof hits === "number" && Number.isInteger(hits)) return { hits };
88
+ return fail(reader, at, {
89
+ message: "expected null or { hits: integer }",
90
+ fallback: null
91
+ });
92
+ }
93
+ function toJson(value) {
94
+ if (value === null || typeof value === "boolean" || typeof value === "string") return value;
95
+ if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
96
+ if (Array.isArray(value)) {
97
+ const items = value.map(toJson);
98
+ return items.every((item) => item !== void 0) ? items.filter((item) => item !== void 0) : void 0;
99
+ }
100
+ if (!isRecord(value)) return void 0;
101
+ const entries = Object.entries(value).map(([key, child]) => [key, toJson(child)]);
102
+ if (entries.some(([, child]) => child === void 0)) return void 0;
103
+ return Object.fromEntries(entries.flatMap(([key, child]) => child === void 0 ? [] : [[key, child]]));
104
+ }
105
+ function readJson(reader, at) {
106
+ const value = toJson(at.record[at.key]);
107
+ return value === void 0 ? fail(reader, at, {
108
+ message: "expected JSON value",
109
+ fallback: null
110
+ }) : value;
111
+ }
112
+ function readList(reader, at, item) {
113
+ const value = at.record[at.key];
114
+ if (!Array.isArray(value)) return fail(reader, at, {
115
+ message: "expected array",
116
+ fallback: []
117
+ });
118
+ return value.flatMap((entry, index) => {
119
+ const path = `${at.path}.${at.key}[${index}]`;
120
+ if (isRecord(entry)) return [item(entry, path)];
121
+ report(reader, {
122
+ path,
123
+ message: "expected object"
124
+ });
125
+ return [];
126
+ });
127
+ }
128
+ // src/validate-explanation.ts
129
+ const OPERATORS = [
130
+ "<",
131
+ "<=",
132
+ ">",
133
+ ">=",
134
+ "===",
135
+ "!==",
136
+ "==",
137
+ "!="
138
+ ];
139
+ const SOURCES = ["payload", "context"];
140
+ function readLocation(reader, record, path) {
141
+ return {
142
+ file: readString(reader, {
143
+ record,
144
+ key: "file",
145
+ path
146
+ }),
147
+ line: readInteger(reader, {
148
+ record,
149
+ key: "line",
150
+ path
151
+ }),
152
+ column: readInteger(reader, {
153
+ record,
154
+ key: "column",
155
+ path
156
+ })
157
+ };
158
+ }
159
+ function readNullableLocation(reader, record, path) {
160
+ const value = record["location"];
161
+ if (value === null) return null;
162
+ if (isRecord(value)) return readLocation(reader, value, `${path}.location`);
163
+ report(reader, {
164
+ path: `${path}.location`,
165
+ message: "expected null or object"
166
+ });
167
+ return null;
168
+ }
169
+ function readTarget(reader, record, path) {
170
+ const value = record["target"];
171
+ const at = `${path}.target`;
172
+ if (!isRecord(value)) {
173
+ report(reader, {
174
+ path: at,
175
+ message: "expected object"
176
+ });
177
+ return { kind: "stay" };
178
+ }
179
+ const kind = readEnum(reader, {
180
+ record: value,
181
+ key: "kind",
182
+ path: at
183
+ }, [
184
+ "state",
185
+ "stay",
186
+ "unresolved"
187
+ ]);
188
+ if (kind === "state") return {
189
+ kind,
190
+ state: readString(reader, {
191
+ record: value,
192
+ key: "state",
193
+ path: at
194
+ })
195
+ };
196
+ if (kind === "unresolved") return {
197
+ kind,
198
+ text: readString(reader, {
199
+ record: value,
200
+ key: "text",
201
+ path: at
202
+ })
203
+ };
204
+ return { kind: "stay" };
205
+ }
206
+ function readBranch(reader, record, path) {
207
+ return {
208
+ conditions: readStrings(reader, {
209
+ record,
210
+ key: "conditions",
211
+ path
212
+ }),
213
+ target: readTarget(reader, record, path)
214
+ };
215
+ }
216
+ function readComparison(reader, record, path) {
217
+ const literal = record["literal"];
218
+ const valid = typeof literal === "number" || typeof literal === "string" || typeof literal === "boolean";
219
+ if (!valid) report(reader, {
220
+ path: `${path}.literal`,
221
+ message: "expected number, string or boolean"
222
+ });
223
+ return {
224
+ source: readEnum(reader, {
225
+ record,
226
+ key: "source",
227
+ path
228
+ }, SOURCES) ?? "payload",
229
+ path: readString(reader, {
230
+ record,
231
+ key: "path",
232
+ path
233
+ }),
234
+ operator: readEnum(reader, {
235
+ record,
236
+ key: "operator",
237
+ path
238
+ }, OPERATORS) ?? "===",
239
+ literal: valid ? literal : 0
240
+ };
241
+ }
242
+ function readLiterals(reader, record, path) {
243
+ const value = record["literals"];
244
+ if (Array.isArray(value) && value.every((item) => typeof item === "number" || typeof item === "string")) return value.filter((item) => typeof item === "number" || typeof item === "string");
245
+ report(reader, {
246
+ path: `${path}.literals`,
247
+ message: "expected (number | string)[]"
248
+ });
249
+ return [];
250
+ }
251
+ function readAnalyzed(reader, record, path) {
252
+ const location = record["location"];
253
+ return {
254
+ status: "analyzed",
255
+ location: isRecord(location) ? readLocation(reader, location, `${path}.location`) : readLocation(reader, {}, `${path}.location`),
256
+ registration: readEnum(reader, {
257
+ record,
258
+ key: "registration",
259
+ path
260
+ }, ["on", "onAny"]) ?? "on",
261
+ branches: readList(reader, {
262
+ record,
263
+ key: "branches",
264
+ path
265
+ }, (item, at) => readBranch(reader, item, at)),
266
+ payloadReads: readStrings(reader, {
267
+ record,
268
+ key: "payloadReads",
269
+ path
270
+ }),
271
+ contextReads: readStrings(reader, {
272
+ record,
273
+ key: "contextReads",
274
+ path
275
+ }),
276
+ contextWrites: readStrings(reader, {
277
+ record,
278
+ key: "contextWrites",
279
+ path
280
+ }),
281
+ readsActorState: readBoolean(reader, {
282
+ record,
283
+ key: "readsActorState",
284
+ path
285
+ }),
286
+ comparisons: readList(reader, {
287
+ record,
288
+ key: "comparisons",
289
+ path
290
+ }, (item, at) => readComparison(reader, item, at)),
291
+ literals: readLiterals(reader, record, path)
292
+ };
293
+ }
294
+ function readExplanation(reader, at) {
295
+ const value = at.record["explanation"];
296
+ const path = `${at.path}.explanation`;
297
+ if (value === null) return null;
298
+ if (!isRecord(value)) {
299
+ report(reader, {
300
+ path,
301
+ message: "expected null or object"
302
+ });
303
+ return null;
304
+ }
305
+ if (readEnum(reader, {
306
+ record: value,
307
+ key: "status",
308
+ path
309
+ }, ["analyzed", "not_analyzed"]) === "analyzed") return readAnalyzed(reader, value, path);
310
+ return {
311
+ status: "not_analyzed",
312
+ location: readNullableLocation(reader, value, path),
313
+ reason: readString(reader, {
314
+ record: value,
315
+ key: "reason",
316
+ path
317
+ }),
318
+ snippet: readNullableString(reader, {
319
+ record: value,
320
+ key: "snippet",
321
+ path
322
+ })
323
+ };
324
+ }
325
+ // src/validate-fuzz.ts
326
+ function readStep(reader, record, path) {
327
+ if (record["kind"] === "advance") return {
328
+ kind: "advance",
329
+ ms: readInteger(reader, {
330
+ record,
331
+ key: "ms",
332
+ path
333
+ })
334
+ };
335
+ if (record["kind"] !== "event") report(reader, {
336
+ path: `${path}.kind`,
337
+ message: "expected event or advance"
338
+ });
339
+ return {
340
+ kind: "event",
341
+ event: readString(reader, {
342
+ record,
343
+ key: "event",
344
+ path
345
+ }),
346
+ payload: readJson(reader, {
347
+ record,
348
+ key: "payload",
349
+ path
350
+ })
351
+ };
352
+ }
353
+ function readSteps(reader, record, path) {
354
+ return readList(reader, {
355
+ record,
356
+ key: "steps",
357
+ path
358
+ }, (item, at) => readStep(reader, item, at));
359
+ }
360
+ function readSequence(reader, record, path) {
361
+ return {
362
+ steps: readSteps(reader, record, path),
363
+ discovered: readStrings(reader, {
364
+ record,
365
+ key: "discovered",
366
+ path
367
+ })
368
+ };
369
+ }
370
+ function readError(reader, record, path) {
371
+ return {
372
+ from: readString(reader, {
373
+ record,
374
+ key: "from",
375
+ path
376
+ }),
377
+ event: readString(reader, {
378
+ record,
379
+ key: "event",
380
+ path
381
+ }),
382
+ message: readString(reader, {
383
+ record,
384
+ key: "message",
385
+ path
386
+ }),
387
+ steps: readSteps(reader, record, path)
388
+ };
389
+ }
390
+ function readFuzz(reader, at) {
391
+ const value = at.record["fuzz"];
392
+ if (value === null) return null;
393
+ const path = `${at.path}.fuzz`;
394
+ if (!isRecord(value)) {
395
+ report(reader, {
396
+ path,
397
+ message: "expected null or object"
398
+ });
399
+ return null;
400
+ }
401
+ return {
402
+ seed: readInteger(reader, {
403
+ record: value,
404
+ key: "seed",
405
+ path
406
+ }),
407
+ iterations: readInteger(reader, {
408
+ record: value,
409
+ key: "iterations",
410
+ path
411
+ }),
412
+ corpus: readList(reader, {
413
+ record: value,
414
+ key: "corpus",
415
+ path
416
+ }, (item, itemPath) => readSequence(reader, item, itemPath)),
417
+ errors: readList(reader, {
418
+ record: value,
419
+ key: "errors",
420
+ path
421
+ }, (item, itemPath) => readError(reader, item, itemPath))
422
+ };
423
+ }
424
+ // src/validate.ts
425
+ const EDGE_KINDS = [
426
+ "initial",
427
+ "transition",
428
+ "effect",
429
+ "stay"
430
+ ];
431
+ const EDGE_ORIGINS = [
432
+ "declared",
433
+ "probe",
434
+ "fuzz"
435
+ ];
436
+ function readState(reader, record, path) {
437
+ return {
438
+ id: readString(reader, {
439
+ record,
440
+ key: "id",
441
+ path
442
+ }),
443
+ label: readString(reader, {
444
+ record,
445
+ key: "label",
446
+ path
447
+ }),
448
+ region: readNullableString(reader, {
449
+ record,
450
+ key: "region",
451
+ path
452
+ }),
453
+ initial: readBoolean(reader, {
454
+ record,
455
+ key: "initial",
456
+ path
457
+ }),
458
+ final: readBoolean(reader, {
459
+ record,
460
+ key: "final",
461
+ path
462
+ }),
463
+ effects: readList(reader, {
464
+ record,
465
+ key: "effects",
466
+ path
467
+ }, (item, at) => ({
468
+ name: readString(reader, {
469
+ record: item,
470
+ key: "name",
471
+ path: at
472
+ }),
473
+ sends: readNullableStrings(reader, {
474
+ record: item,
475
+ key: "sends",
476
+ path: at
477
+ })
478
+ })),
479
+ coverage: readCoverage(reader, {
480
+ record,
481
+ key: "coverage",
482
+ path
483
+ }),
484
+ tested: readCoverage(reader, {
485
+ record,
486
+ key: "tested",
487
+ path
488
+ })
489
+ };
490
+ }
491
+ function readEvent(reader, record, path) {
492
+ if (record["payloadType"] !== null) report(reader, {
493
+ path: `${path}.payloadType`,
494
+ message: "expected null"
495
+ });
496
+ return {
497
+ id: readString(reader, {
498
+ record,
499
+ key: "id",
500
+ path
501
+ }),
502
+ internal: readBoolean(reader, {
503
+ record,
504
+ key: "internal",
505
+ path
506
+ }),
507
+ payloadType: null
508
+ };
509
+ }
510
+ function readEdge(reader, record, path) {
511
+ return {
512
+ id: readString(reader, {
513
+ record,
514
+ key: "id",
515
+ path
516
+ }),
517
+ from: readString(reader, {
518
+ record,
519
+ key: "from",
520
+ path
521
+ }),
522
+ event: readString(reader, {
523
+ record,
524
+ key: "event",
525
+ path
526
+ }),
527
+ to: readNullableString(reader, {
528
+ record,
529
+ key: "to",
530
+ path
531
+ }),
532
+ kind: readEnum(reader, {
533
+ record,
534
+ key: "kind",
535
+ path
536
+ }, EDGE_KINDS) ?? "transition",
537
+ origin: readEnum(reader, {
538
+ record,
539
+ key: "origin",
540
+ path
541
+ }, EDGE_ORIGINS) ?? "probe",
542
+ emits: readStrings(reader, {
543
+ record,
544
+ key: "emits",
545
+ path
546
+ }),
547
+ contexts: readStrings(reader, {
548
+ record,
549
+ key: "contexts",
550
+ path
551
+ }),
552
+ explanation: readExplanation(reader, {
553
+ record,
554
+ path
555
+ }),
556
+ coverage: readCoverage(reader, {
557
+ record,
558
+ key: "coverage",
559
+ path
560
+ }),
561
+ tested: readCoverage(reader, {
562
+ record,
563
+ key: "tested",
564
+ path
565
+ })
566
+ };
567
+ }
568
+ /** Edge endpoints must name declared states; a dangling reference is a broken document. */
569
+ function checkReferences(reader, machine, path) {
570
+ const stateIds = new Set(machine.states.map((state) => state.id));
571
+ const known = (id) => stateIds.has(id) || id === "__initial__" || isAnyState(id);
572
+ machine.edges.forEach((edge, index) => {
573
+ if (!known(edge.from)) report(reader, {
574
+ path: `${path}.edges[${index}].from`,
575
+ message: `unknown state ${edge.from}`
576
+ });
577
+ const anyStay = edge.kind === "stay" && edge.to === edge.from && isAnyState(edge.from);
578
+ if (edge.to !== null && !stateIds.has(edge.to) && !anyStay) report(reader, {
579
+ path: `${path}.edges[${index}].to`,
580
+ message: `unknown state ${edge.to}`
581
+ });
582
+ });
583
+ if (machine.initial !== null && !stateIds.has(machine.initial)) report(reader, {
584
+ path: `${path}.initial`,
585
+ message: `unknown state ${machine.initial}`
586
+ });
587
+ }
588
+ function readMachine(reader, record, path) {
589
+ const machine = {
590
+ id: readString(reader, {
591
+ record,
592
+ key: "id",
593
+ path
594
+ }),
595
+ initial: readNullableString(reader, {
596
+ record,
597
+ key: "initial",
598
+ path
599
+ }),
600
+ regions: readStrings(reader, {
601
+ record,
602
+ key: "regions",
603
+ path
604
+ }),
605
+ states: readList(reader, {
606
+ record,
607
+ key: "states",
608
+ path
609
+ }, (item, at) => readState(reader, item, at)),
610
+ events: readList(reader, {
611
+ record,
612
+ key: "events",
613
+ path
614
+ }, (item, at) => readEvent(reader, item, at)),
615
+ edges: readList(reader, {
616
+ record,
617
+ key: "edges",
618
+ path
619
+ }, (item, at) => readEdge(reader, item, at)),
620
+ fuzz: readFuzz(reader, {
621
+ record,
622
+ path
623
+ })
624
+ };
625
+ checkReferences(reader, machine, path);
626
+ return machine;
627
+ }
628
+ /**
629
+ * Read an untrusted value (parsed JSON) as an viz document. Returns every
630
+ * issue found, not just the first, so a broken file can be fixed in one pass.
631
+ */
632
+ function validateViz(value) {
633
+ const reader = { issues: [] };
634
+ if (!isRecord(value)) return Either.left([{
635
+ path: "$",
636
+ message: "expected object"
637
+ }]);
638
+ if (value["schemaVersion"] !== 7) return Either.left([{
639
+ path: "$.schemaVersion",
640
+ message: `expected 7, got ${JSON.stringify(value["schemaVersion"])}`
641
+ }]);
642
+ const machines = readList(reader, {
643
+ record: value,
644
+ key: "machines",
645
+ path: "$"
646
+ }, (item, at) => readMachine(reader, item, at));
647
+ if (reader.issues.length > 0) return Either.left(reader.issues);
648
+ const document = {
649
+ schemaVersion: 7,
650
+ machines
651
+ };
652
+ return Either.right(document);
653
+ }
654
+ /** Parse JSON text and validate it. JSON syntax errors become an issue at `$`. */
655
+ function parseViz(text) {
656
+ const parsed = Either.from(() => JSON.parse(text));
657
+ if (Either.isLeft(parsed)) {
658
+ const reason = parsed[0] instanceof Error ? parsed[0].message : String(parsed[0]);
659
+ return Either.left([{
660
+ path: "$",
661
+ message: `invalid JSON: ${reason}`
662
+ }]);
663
+ }
664
+ return validateViz(parsed[1]);
665
+ }
666
+ // src/moves.ts
667
+ const GROUP_ORDER = {
668
+ input: 0,
669
+ effect: 1,
670
+ internal: 2
671
+ };
672
+ function targetsOf(edge, current) {
673
+ if (edge.kind === "stay") return [current];
674
+ if (edge.to !== null) return [edge.to];
675
+ if (edge.explanation?.status !== "analyzed") return [];
676
+ const targets = edge.explanation.branches.flatMap((branch) => {
677
+ if (branch.target.kind === "state") return [branch.target.state];
678
+ return branch.target.kind === "stay" ? [current] : [];
679
+ });
680
+ return [...new Set(targets)].sort();
681
+ }
682
+ function sendersOf(machine, state) {
683
+ const senders = /* @__PURE__ */ new Map();
684
+ const effects = machine.states.find((each) => each.id === state)?.effects ?? [];
685
+ for (const effect of effects) for (const sent of effect.sends ?? []) senders.set(sent, [...senders.get(sent) ?? [], effect.name]);
686
+ return senders;
687
+ }
688
+ /**
689
+ * Every edge an event can take out of `state`: its own registrations and the
690
+ * `onAny` ones. A final state has none: the actor has stopped.
691
+ */
692
+ function movesFrom(machine, state) {
693
+ const current = machine.states.find((each) => each.id === state);
694
+ if (current === void 0 || current.final) return [];
695
+ const internal = new Set(machine.events.filter((each) => each.internal).map((each) => each.id));
696
+ const senders = sendersOf(machine, state);
697
+ return machine.edges.filter((edge) => (edge.kind === "transition" || edge.kind === "stay") && (edge.from === state || coveredByAny(edge.from, state))).map((edge) => {
698
+ const sentBy = senders.get(edge.event) ?? [];
699
+ const group = !internal.has(edge.event) ? "input" : sentBy.length > 0 ? "effect" : "internal";
700
+ return {
701
+ edge,
702
+ targets: targetsOf(edge, state),
703
+ group,
704
+ sentBy
705
+ };
706
+ }).sort((left, right) => GROUP_ORDER[left.group] - GROUP_ORDER[right.group] || left.edge.event.localeCompare(right.edge.event) || left.edge.id.localeCompare(right.edge.id));
707
+ }
708
+ /** Where a path ends: the last step's target, or the initial state. */
709
+ function stateAfter(machine, path) {
710
+ return path.at(-1)?.to ?? machine.initial;
711
+ }
712
+ /** Events an effect of `state` declares in `sends` that no handler in `state` takes. */
713
+ function unhandledSends(machine, state) {
714
+ const handled = new Set(movesFrom(machine, state).map((move) => move.edge.event));
715
+ return (machine.states.find((each) => each.id === state)?.effects ?? []).flatMap((effect) => (effect.sends ?? []).filter((event) => !handled.has(event)).map((event) => ({
716
+ effect: effect.name,
717
+ event
718
+ })));
719
+ }
720
+ /** One step as text for a URL: the edge id, plus `~target` when the edge has no single target. */
721
+ function encodeStep(machine, step) {
722
+ const edge = machine.edges.find((each) => each.id === step.edge);
723
+ return edge !== void 0 && edge.to === null && edge.kind !== "stay" ? `${step.edge}~${step.to}` : step.edge;
724
+ }
725
+ /**
726
+ * Rebuild a path from encoded steps (e.g. `?step=` in a shared link). Stops at
727
+ * the first step the graph does not allow from where the path has got to, so
728
+ * a link from an older document still opens at its last valid point.
729
+ */
730
+ function decodePath(machine, encoded) {
731
+ const path = [];
732
+ for (const text of encoded) {
733
+ const cut = text.lastIndexOf("~");
734
+ const edgeId = cut === -1 ? text : text.slice(0, cut);
735
+ const from = stateAfter(machine, path);
736
+ const move = from === null ? void 0 : movesFrom(machine, from).find((each) => each.edge.id === edgeId);
737
+ const to = cut === -1 && move?.targets.length === 1 ? move.targets[0] : text.slice(cut + 1);
738
+ if (move === void 0 || to === void 0 || !move.targets.includes(to)) break;
739
+ path.push({
740
+ edge: edgeId,
741
+ to
742
+ });
743
+ }
744
+ return path;
745
+ }
746
+ // src/routing.ts
747
+ /**
748
+ * `onAny` handlers that pick their target from the current state. Drawn from
749
+ * `*`, they claim every declared target from every state; per-state `m.on`
750
+ * registrations would draw the exact source/target pairs.
751
+ */
752
+ function routingWarnings(machine) {
753
+ return [...new Set(machine.edges.filter((edge) => isAnyState(edge.from) && edge.explanation?.status === "analyzed" && edge.explanation.readsActorState).map((edge) => edge.event))].sort().map((event) => ({
754
+ machine: machine.id,
755
+ event,
756
+ message: `onAny(${event}) reads actor state to pick its target, so the diagram cannot tell which state leads where. Register m.on per state instead.`
757
+ }));
758
+ }
759
+ export { stateAfter as a, validateViz as c, coveredByAny as d, isAnyState as f, movesFrom as i, ANY_STATE as l, decodePath as n, unhandledSends as o, scopeOf as p, encodeStep as r, parseViz as s, routingWarnings as t, VIZ_SCHEMA_VERSION as u };