@mantaq/inspect 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,1790 @@
1
+ import { ANY_STATE, VIZ_SCHEMA_VERSION, coveredByAny, isAnyState, routingWarnings, unhandledSends } from "@mantaq/viz";
2
+ import { INITIAL_NODE_ID, buildGraph, parseInternalEventIds } from "@mantaq/traversal";
3
+ import { Either } from "@mantaq/utils";
4
+ import ts from "typescript";
5
+ import { dirname, join, relative, sep } from "node:path";
6
+ import { DEFAULT_SEED } from "@mantaq/pbt";
7
+ import { VirtualClock } from "@mantaq/core";
8
+ import { existsSync } from "node:fs";
9
+ import { fileURLToPath } from "node:url";
10
+ import { runnerImport } from "vite";
11
+ // src/structure.ts
12
+ function byId$1(left, right) {
13
+ if (left.id < right.id) return -1;
14
+ return left.id > right.id ? 1 : 0;
15
+ }
16
+ function regionOf(nodeId) {
17
+ const cut = nodeId.lastIndexOf(".");
18
+ return cut === -1 ? null : nodeId.slice(0, cut);
19
+ }
20
+ function countBy(keys) {
21
+ const counts = /* @__PURE__ */ new Map();
22
+ for (const key of keys) counts.set(key, (counts.get(key) ?? 0) + 1);
23
+ return counts;
24
+ }
25
+ function hits(count) {
26
+ return { hits: count ?? 0 };
27
+ }
28
+ function toState(node, context) {
29
+ const { visits } = context;
30
+ return {
31
+ id: node.id,
32
+ label: node.label,
33
+ region: regionOf(node.id),
34
+ initial: node.id === context.initialId,
35
+ final: node.isFinal,
36
+ effects: context.effects.find((edge) => edge.source === node.id)?.effects ?? [],
37
+ coverage: visits ? hits(visits.get(node.id)) : null,
38
+ tested: null
39
+ };
40
+ }
41
+ function edgeKind(edge) {
42
+ if (edge.source === INITIAL_NODE_ID) return "initial";
43
+ if (edge.isStay === true) return "stay";
44
+ return edge.isInternal === true && edge.label.startsWith("effect:") ? "effect" : "transition";
45
+ }
46
+ function emitsOf(edge) {
47
+ const match = (edge.payload?.action)?.match(/^emit\((.*)\)$/);
48
+ return match?.[1] ? match[1].split(", ").sort() : [];
49
+ }
50
+ /** Count of recorded transitions matching an edge by source, event and (if determined) target. */
51
+ function edgeHits$1(edge, transitions) {
52
+ if (isAnyState(edge.source)) {
53
+ let total = 0;
54
+ for (const [key, byTarget] of transitions) {
55
+ const [from = "", event] = key.split("\0");
56
+ if (event !== edge.label || !coveredByAny(edge.source, from)) continue;
57
+ total += isAnyHit(edge, byTarget);
58
+ }
59
+ return total;
60
+ }
61
+ const byTarget = transitions.get(`${edge.source}\u0000${edge.label}`);
62
+ if (!byTarget) return 0;
63
+ if (edge.isUndetermined === true) return [...byTarget.values()].reduce((sum, count) => sum + count, 0);
64
+ return byTarget.get(edge.target) ?? 0;
65
+ }
66
+ /** Hits of an `onAny` edge from one source: its target, or every outcome when undetermined. */
67
+ function isAnyHit(edge, byTarget) {
68
+ if (edge.isUndetermined === true) return [...byTarget.values()].reduce((sum, n) => sum + n, 0);
69
+ return byTarget.get(edge.target) ?? 0;
70
+ }
71
+ function indexTransitions(history) {
72
+ const index = /* @__PURE__ */ new Map();
73
+ for (const record of history.transitions()) {
74
+ const key = `${record.from}\u0000${record.event}`;
75
+ const byTarget = index.get(key) ?? /* @__PURE__ */ new Map();
76
+ const target = record.to ?? "";
77
+ byTarget.set(target, (byTarget.get(target) ?? 0) + 1);
78
+ index.set(key, byTarget);
79
+ }
80
+ return index;
81
+ }
82
+ function toEdge(edge, transitions) {
83
+ const kind = edgeKind(edge);
84
+ return {
85
+ id: edge.id,
86
+ from: edge.source,
87
+ event: kind === "initial" ? "" : edge.label,
88
+ to: edge.isUndetermined === true ? null : edge.target,
89
+ kind,
90
+ origin: edge.isDeclared === true ? "declared" : "probe",
91
+ emits: emitsOf(edge),
92
+ contexts: [...new Set(edge.contexts)].sort(),
93
+ explanation: null,
94
+ coverage: transitions && kind === "transition" ? hits(edgeHits$1(edge, transitions)) : null,
95
+ tested: null
96
+ };
97
+ }
98
+ /** One machine; `toViz` is this plus sorting into a document. */
99
+ function toVizMachine(input) {
100
+ const graph = buildGraph(input.actor, { sampleContexts: input.sampleContexts });
101
+ const internalIds = parseInternalEventIds(input.actor.options);
102
+ const initial = graph.edges.find((edge) => edge.source === INITIAL_NODE_ID)?.target ?? null;
103
+ const visits = input.history ? countBy(input.history.stateVisits().map((visit) => visit.stateName)) : void 0;
104
+ const transitions = input.history ? indexTransitions(input.history) : void 0;
105
+ const effectEdges = graph.edges.filter((edge) => edgeKind(edge) === "effect");
106
+ const states = graph.nodes.filter((node) => node.id !== INITIAL_NODE_ID && node.isAny !== true).map((node) => toState(node, {
107
+ visits,
108
+ initialId: initial,
109
+ effects: effectEdges
110
+ })).sort(byId$1);
111
+ const edges = graph.edges.map((edge) => toEdge(edge, transitions)).sort(byId$1);
112
+ const events = [...new Set(edges.map((edge) => edge.event).filter(Boolean))].filter((eventId) => !eventId.startsWith("effect:")).sort().map((eventId) => ({
113
+ id: eventId,
114
+ internal: internalIds.has(eventId),
115
+ payloadType: null
116
+ }));
117
+ const regions = [...new Set(states.flatMap((state) => state.region ? [state.region] : []))].sort();
118
+ return {
119
+ id: input.id,
120
+ initial,
121
+ regions,
122
+ states,
123
+ events,
124
+ edges,
125
+ fuzz: null
126
+ };
127
+ }
128
+ function toViz(inputs) {
129
+ return {
130
+ schemaVersion: VIZ_SCHEMA_VERSION,
131
+ machines: inputs.map(toVizMachine).sort(byId$1)
132
+ };
133
+ }
134
+ // src/fuzz/mutate.ts
135
+ const REPEAT_COUNTS = [
136
+ 2,
137
+ 3,
138
+ 5,
139
+ 8,
140
+ 13
141
+ ];
142
+ const MAX_REPEAT = 64;
143
+ const ADVANCES = [
144
+ 1,
145
+ 10,
146
+ 100,
147
+ 500,
148
+ 1e3,
149
+ 5e3
150
+ ];
151
+ function isObject(value) {
152
+ return typeof value === "object" && value !== null && !Array.isArray(value);
153
+ }
154
+ function freshEvent(context) {
155
+ const event = context.rng.pick(context.events) ?? "";
156
+ const samples = context.payloads[event] ?? [];
157
+ return {
158
+ kind: "event",
159
+ event,
160
+ payload: context.rng.pick(samples) ?? null
161
+ };
162
+ }
163
+ function freshStep(context) {
164
+ if (context.rng.chance(.1)) return {
165
+ kind: "advance",
166
+ ms: context.rng.pick(ADVANCES) ?? 1
167
+ };
168
+ return freshEvent(context);
169
+ }
170
+ function mutateNumber(value, context) {
171
+ const { rng, literals } = context;
172
+ if (literals.numbers.length > 0 && rng.chance(.5)) return rng.pick(literals.numbers) ?? value;
173
+ const options = [
174
+ 0,
175
+ -1,
176
+ 1,
177
+ value + 1,
178
+ value - 1,
179
+ value * 2,
180
+ Math.trunc(value / 2),
181
+ rng.int(2001) - 1e3
182
+ ];
183
+ return options[rng.int(options.length)] ?? value;
184
+ }
185
+ function mutateString(value, context) {
186
+ const { rng, literals } = context;
187
+ if (literals.strings.length > 0 && rng.chance(.5)) return rng.pick(literals.strings) ?? value;
188
+ const options = [
189
+ "",
190
+ "x",
191
+ `${value}x`,
192
+ value.slice(1),
193
+ value.toUpperCase()
194
+ ];
195
+ return options[rng.int(options.length)] ?? value;
196
+ }
197
+ /**
198
+ * Replace one leaf somewhere inside a JSON value, keeping its JSON type. Keys
199
+ * are never dropped: payloads stay shaped like the samples, so a handler that
200
+ * throws is a real finding, not a type violation the compiler already rejects.
201
+ */
202
+ function mutateJson(value, context) {
203
+ if (typeof value === "number") return mutateNumber(value, context);
204
+ if (typeof value === "string") return mutateString(value, context);
205
+ if (typeof value === "boolean") return !value;
206
+ if (Array.isArray(value)) {
207
+ if (value.length === 0) return value;
208
+ const at = context.rng.int(value.length);
209
+ return value.map((item, index) => index === at ? mutateJson(item, context) : item);
210
+ }
211
+ if (!isObject(value)) return value;
212
+ const keys = Object.keys(value).sort();
213
+ const key = context.rng.pick(keys);
214
+ if (key === void 0) return value;
215
+ return {
216
+ ...value,
217
+ [key]: mutateJson(value[key] ?? null, context)
218
+ };
219
+ }
220
+ function replaceAt(steps, at, step) {
221
+ return steps.map((current, index) => index === at ? step : current);
222
+ }
223
+ function mutatePayload(steps, context) {
224
+ const withPayload = steps.flatMap((step, index) => step.kind === "event" && step.payload !== null ? [index] : []);
225
+ const at = context.rng.pick(withPayload);
226
+ const step = at === void 0 ? void 0 : steps[at];
227
+ if (at === void 0 || step?.kind !== "event") return steps;
228
+ return replaceAt(steps, at, {
229
+ ...step,
230
+ payload: mutateJson(step.payload, context)
231
+ });
232
+ }
233
+ function insert(steps, context) {
234
+ if (steps.length >= context.maxLength) return steps;
235
+ const at = context.rng.chance(.6) ? steps.length : context.rng.int(steps.length + 1);
236
+ return [
237
+ ...steps.slice(0, at),
238
+ freshStep(context),
239
+ ...steps.slice(at)
240
+ ];
241
+ }
242
+ function remove(steps, context) {
243
+ if (steps.length === 0) return steps;
244
+ const at = context.rng.int(steps.length);
245
+ return steps.filter((_, index) => index !== at);
246
+ }
247
+ function swap(steps, context) {
248
+ if (steps.length < 2) return steps;
249
+ const at = context.rng.int(steps.length - 1);
250
+ const left = steps[at];
251
+ const right = steps[at + 1];
252
+ if (left === void 0 || right === void 0) return steps;
253
+ return replaceAt(replaceAt(steps, at, right), at + 1, left);
254
+ }
255
+ function advance(steps, context) {
256
+ if (steps.length >= context.maxLength) return steps;
257
+ const at = context.rng.int(steps.length + 1);
258
+ const ms = context.rng.pick(ADVANCES) ?? 1;
259
+ return [
260
+ ...steps.slice(0, at),
261
+ {
262
+ kind: "advance",
263
+ ms
264
+ },
265
+ ...steps.slice(at)
266
+ ];
267
+ }
268
+ const OPERATORS = [
269
+ insert,
270
+ insert,
271
+ insert,
272
+ remove,
273
+ swap,
274
+ mutatePayload,
275
+ mutatePayload,
276
+ advance
277
+ ];
278
+ /** Loop lengths to try: small counts plus any integer literal a handler compares against. */
279
+ function repeatCounts(context) {
280
+ const fromLiterals = context.literals.numbers.filter((value) => Number.isInteger(value) && value >= 2 && value <= MAX_REPEAT);
281
+ return [...REPEAT_COUNTS, ...fromLiterals];
282
+ }
283
+ /**
284
+ * Loop a step that changed the actor's context (a counter, an accumulating
285
+ * list): `if (retries > 10)` needs the same event many times, and repeating
286
+ * a step that changes nothing would only add noise. Inserts `count` copies.
287
+ */
288
+ function repeat(parent, context) {
289
+ const at = context.rng.pick(parent.contextSteps);
290
+ const step = at === void 0 ? void 0 : parent.steps[at];
291
+ if (at === void 0 || step === void 0) return [...parent.steps];
292
+ const room = context.maxLength * 4 - parent.steps.length;
293
+ const count = Math.min(context.rng.pick(repeatCounts(context)) ?? 2, room);
294
+ if (count <= 0) return [...parent.steps];
295
+ const copies = Array.from({ length: count }, () => step);
296
+ return [
297
+ ...parent.steps.slice(0, at + 1),
298
+ ...copies,
299
+ ...parent.steps.slice(at + 1)
300
+ ];
301
+ }
302
+ /**
303
+ * Apply 1-3 random operators: insert, delete, swap, payload tweak, time
304
+ * advance. Sometimes first loops a context-changing step of the parent.
305
+ */
306
+ function mutate(parent, context) {
307
+ const loop = context.repeat && parent.contextSteps.length > 0 && context.rng.chance(.2);
308
+ let child = loop ? repeat(parent, context) : [...parent.steps];
309
+ const rounds = (loop ? 0 : 1) + context.rng.int(3);
310
+ for (let round = 0; round < rounds; round += 1) child = (context.rng.pick(OPERATORS) ?? insert)(child, context);
311
+ return child;
312
+ }
313
+ // src/fuzz/rng.ts
314
+ function createRng(seed) {
315
+ let state = seed >>> 0;
316
+ function next() {
317
+ state = state + 1831565813 >>> 0;
318
+ let mixed = state;
319
+ mixed = Math.imul(mixed ^ mixed >>> 15, mixed | 1);
320
+ mixed ^= mixed + Math.imul(mixed ^ mixed >>> 7, mixed | 61);
321
+ return ((mixed ^ mixed >>> 14) >>> 0) / 4294967296;
322
+ }
323
+ function int(bound) {
324
+ return Math.floor(next() * bound);
325
+ }
326
+ return {
327
+ next,
328
+ int,
329
+ pick: (items) => items[int(items.length)],
330
+ chance: (probability) => next() < probability
331
+ };
332
+ }
333
+ // src/fuzz/run.ts
334
+ const ERROR_STATE = "__error";
335
+ function eventTypes(options, key) {
336
+ if (typeof options !== "object" || options === null) return [];
337
+ const list = Reflect.get(options, key);
338
+ if (!Array.isArray(list)) return [];
339
+ return list.flatMap((entry) => {
340
+ if (typeof entry !== "object" || entry === null) return [];
341
+ const type = Reflect.get(entry, "type");
342
+ return typeof type === "string" ? [type] : [];
343
+ });
344
+ }
345
+ /** Declared inputs and internal events, each sorted. */
346
+ function eventCatalog(actor) {
347
+ return {
348
+ inputs: eventTypes(actor.options, "inputs").sort(),
349
+ internal: eventTypes(actor.options, "internal").sort()
350
+ };
351
+ }
352
+ /** onAny handlers live under this key in the dispatch map (core builder). */
353
+ const ANY_KEY = "Any";
354
+ function handles(actor, event) {
355
+ const transitions = actor.options?.transitions;
356
+ return transitions?.[actor.state.name]?.[event] !== void 0 || transitions?.[ANY_KEY]?.[event] !== void 0;
357
+ }
358
+ function toEvent(step) {
359
+ return step.payload === null ? { type: step.event } : {
360
+ type: step.event,
361
+ payload: step.payload
362
+ };
363
+ }
364
+ function messageOf(error) {
365
+ return error instanceof Error ? `${error.name}: ${error.message}` : String(error);
366
+ }
367
+ function featuresOf(trace) {
368
+ const features = /* @__PURE__ */ new Set();
369
+ for (const state of trace.visited) features.add(`state:${state}`);
370
+ for (const seen of trace.observations) features.add(seen.transitioned ? `edge:${seen.from}>${seen.event}>${seen.to}` : `stay:${seen.from}>${seen.event}`);
371
+ for (const error of trace.errors) features.add(`error:${error.from}>${error.event}`);
372
+ return [...features].sort();
373
+ }
374
+ /** JSON text of the context, or null when it is not serializable (functions, cycles). */
375
+ function contextText(actor) {
376
+ const text = Either.from(() => JSON.stringify(actor.context ?? null));
377
+ return Either.isRight(text) ? text[1] : null;
378
+ }
379
+ function parsed(text) {
380
+ return text === null ? null : JSON.parse(text);
381
+ }
382
+ function dispatch(actor, context) {
383
+ const { step } = context;
384
+ if (step.kind === "advance") context.clock.advance(step.ms);
385
+ else if (context.internal.has(step.event)) {
386
+ if (handles(actor, step.event)) actor.inject(toEvent(step));
387
+ } else actor.send(toEvent(step));
388
+ }
389
+ /**
390
+ * Replay steps against a fresh actor. Synchronous and deterministic: effects
391
+ * only run via the virtual clock, and nothing here reads wall time.
392
+ */
393
+ function execute(target, steps) {
394
+ const { actor, clock } = target.create();
395
+ const internal = new Set(eventCatalog(actor).internal);
396
+ const visited = [actor.state.name];
397
+ const observations = [];
398
+ const errors = [];
399
+ const contextSteps = [];
400
+ let index = 0;
401
+ actor.on("transition", { fn: ({ event, from, to, transitioned, effects }) => {
402
+ const context = parsed(contextText(actor));
403
+ observations.push({
404
+ from,
405
+ event: event.type,
406
+ to,
407
+ transitioned,
408
+ effects,
409
+ step: index,
410
+ payload: event.payload,
411
+ context
412
+ });
413
+ if (transitioned) visited.push(to);
414
+ } });
415
+ actor.on("error", { fn: (info) => {
416
+ errors.push({
417
+ from: info.state.name,
418
+ event: info.event.type,
419
+ message: messageOf(info.error),
420
+ step: index
421
+ });
422
+ } });
423
+ for (const step of steps) {
424
+ if (actor.state.name === ERROR_STATE) break;
425
+ const before = contextText(actor);
426
+ dispatch(actor, {
427
+ step,
428
+ clock,
429
+ internal
430
+ });
431
+ if (contextText(actor) !== before) contextSteps.push(index);
432
+ index += 1;
433
+ }
434
+ actor.dispose();
435
+ return {
436
+ visited,
437
+ observations,
438
+ errors,
439
+ contextSteps,
440
+ features: featuresOf({
441
+ visited,
442
+ observations,
443
+ errors
444
+ })
445
+ };
446
+ }
447
+ // src/fuzz/fuzz.ts
448
+ function covers(target, steps, wanted) {
449
+ const reached = new Set(execute(target, steps).features);
450
+ return wanted.every((feature) => reached.has(feature));
451
+ }
452
+ /** Greedy one-step deletion, last to first, while `wanted` stays covered. Deterministic. */
453
+ function shrink(target, steps, wanted) {
454
+ let current = [...steps];
455
+ for (let at = current.length - 1; at >= 0; at -= 1) {
456
+ const candidate = current.filter((_, index) => index !== at);
457
+ if (covers(target, candidate, wanted)) current = candidate;
458
+ }
459
+ return current;
460
+ }
461
+ function mutationContext(target, options) {
462
+ const catalog = eventCatalog(target.create().actor);
463
+ return {
464
+ rng: createRng(options.seed),
465
+ events: [...catalog.inputs, ...catalog.internal].sort(),
466
+ payloads: target.payloads ?? {},
467
+ literals: options.literals ?? {
468
+ numbers: [],
469
+ strings: []
470
+ },
471
+ maxLength: options.maxLength ?? 24,
472
+ repeat: options.repeat ?? true
473
+ };
474
+ }
475
+ function collectErrors(target, corpus) {
476
+ const errors = /* @__PURE__ */ new Map();
477
+ for (const entry of corpus) for (const error of execute(target, entry.steps).errors) {
478
+ const key = `${error.from}>${error.event}`;
479
+ if (errors.has(key)) continue;
480
+ const steps = shrink(target, entry.steps.slice(0, error.step + 1), [`error:${key}`]);
481
+ errors.set(key, {
482
+ from: error.from,
483
+ event: error.event,
484
+ message: error.message,
485
+ steps
486
+ });
487
+ }
488
+ return [...errors.values()].sort((left, right) => `${left.from}>${left.event}` < `${right.from}>${right.event}` ? -1 : 1);
489
+ }
490
+ /**
491
+ * Coverage-guided fuzzing. Each iteration mutates a corpus entry, runs it on a
492
+ * fresh actor and keeps it if it reaches a feature (state, edge, stay, error)
493
+ * no earlier run reached. Same target + options => identical result.
494
+ */
495
+ function fuzz(target, options) {
496
+ const context = mutationContext(target, options);
497
+ const seen = /* @__PURE__ */ new Set();
498
+ const firstHit = /* @__PURE__ */ new Map();
499
+ const corpus = [];
500
+ /** Parallel to `corpus`: which steps of each entry changed context. */
501
+ const contextSteps = [];
502
+ function consider(steps, iteration) {
503
+ const trace = execute(target, steps);
504
+ const discovered = trace.features.filter((feature) => !seen.has(feature));
505
+ const guided = options.guide ? options.guide(trace) : false;
506
+ for (const feature of discovered) {
507
+ seen.add(feature);
508
+ firstHit.set(feature, iteration);
509
+ }
510
+ if (discovered.length > 0 || guided) {
511
+ corpus.push({
512
+ steps,
513
+ discovered,
514
+ iteration
515
+ });
516
+ contextSteps.push(trace.contextSteps);
517
+ }
518
+ }
519
+ consider([], 0);
520
+ for (let iteration = 1; iteration <= options.iterations; iteration += 1) {
521
+ const at = context.rng.int(corpus.length);
522
+ consider(mutate({
523
+ steps: corpus[at]?.steps ?? [],
524
+ contextSteps: contextSteps[at] ?? []
525
+ }, context), iteration);
526
+ }
527
+ const kept = corpus.filter((entry) => entry.discovered.length > 0).map((entry) => ({
528
+ ...entry,
529
+ steps: shrink(target, entry.steps, entry.discovered)
530
+ }));
531
+ return {
532
+ seed: options.seed,
533
+ iterations: options.iterations,
534
+ corpus: kept,
535
+ firstHit,
536
+ errors: collectErrors(target, kept)
537
+ };
538
+ }
539
+ // src/fuzz/coverage.ts
540
+ /**
541
+ * An `onAny` edge counts observations from every state it covers. A state's
542
+ * own handler for the same event is not told apart: observations carry no
543
+ * handler identity.
544
+ */
545
+ function matches(edge, seen) {
546
+ if (!(seen.from === edge.from || coveredByAny(edge.from, seen.from)) || seen.event !== edge.event) return false;
547
+ if (edge.kind === "stay") return !seen.transitioned;
548
+ if (edge.to === null) return true;
549
+ return seen.transitioned && seen.to === edge.to;
550
+ }
551
+ /** Times each edge was taken in `traces`; null for edges no handler drives (initial, effect). */
552
+ function edgeHits(edge, traces) {
553
+ if (edge.kind === "initial" || edge.kind === "effect") return null;
554
+ return traces.reduce((sum, trace) => sum + trace.observations.filter((seen) => matches(edge, seen)).length, 0);
555
+ }
556
+ function stateVisits(state, traces) {
557
+ return traces.reduce((sum, trace) => sum + trace.visited.filter((visited) => visited === state).length, 0);
558
+ }
559
+ /**
560
+ * Fill `tested` from replaying test sequences (each from a fresh actor).
561
+ * Lets the viewer show what the tests never exercise, next to what fuzzing reached.
562
+ */
563
+ function withTestCoverage(target, machine, sequences) {
564
+ const traces = sequences.map((steps) => execute(target, steps));
565
+ return {
566
+ ...machine,
567
+ states: machine.states.map((state) => ({
568
+ ...state,
569
+ tested: { hits: stateVisits(state.id, traces) }
570
+ })),
571
+ edges: machine.edges.map((edge) => {
572
+ const hits = edgeHits(edge, traces);
573
+ return {
574
+ ...edge,
575
+ tested: hits === null ? null : { hits }
576
+ };
577
+ })
578
+ };
579
+ }
580
+ // src/fuzz/to-viz.ts
581
+ function observedKey(seen, from) {
582
+ return seen.transitioned ? `${from}>${seen.event}>${seen.to}` : `${from}>${seen.event}`;
583
+ }
584
+ /**
585
+ * Source to draw an observation from: its state when that state registers the
586
+ * event, else the `*` source when an `onAny` handler does. One `*` edge beats
587
+ * the same stay repeated on every state.
588
+ */
589
+ function anySourceOf(machine, seen) {
590
+ return machine.edges.find((edge) => edge.event === seen.event && coveredByAny(edge.from, seen.from))?.from;
591
+ }
592
+ function sourceOf(machine, seen) {
593
+ return machine.edges.some((edge) => edge.from === seen.from && edge.event === seen.event) ? seen.from : anySourceOf(machine, seen) ?? seen.from;
594
+ }
595
+ function fuzzEdge(seen, from) {
596
+ const kind = seen.transitioned ? "transition" : "stay";
597
+ const to = kind === "stay" && from.endsWith(ANY_STATE) ? from : seen.to;
598
+ return {
599
+ id: `fuzz:${from}-${seen.event}-${kind === "stay" ? "stay" : seen.to}`,
600
+ from,
601
+ event: seen.event,
602
+ to,
603
+ kind,
604
+ origin: "fuzz",
605
+ emits: [],
606
+ contexts: [],
607
+ explanation: null,
608
+ coverage: null,
609
+ tested: null
610
+ };
611
+ }
612
+ function knownKey(edge) {
613
+ if (edge.kind === "stay") return `${edge.from}>${edge.event}`;
614
+ return edge.to === null || edge.kind !== "transition" ? null : `${edge.from}>${edge.event}>${edge.to}`;
615
+ }
616
+ /**
617
+ * Edges the fuzzer observed that the structure lacks: concrete targets of
618
+ * undetermined handlers, and `stay` from handlers that declare targets but may
619
+ * also return none. A transition outside `to` would land here too.
620
+ */
621
+ function discoveredEdges(machine, traces) {
622
+ const known = new Set(machine.edges.flatMap((edge) => knownKey(edge) ?? []));
623
+ const found = /* @__PURE__ */ new Map();
624
+ for (const seen of traces.flatMap((trace) => trace.observations)) {
625
+ const from = sourceOf(machine, seen);
626
+ const key = observedKey(seen, from);
627
+ if ([seen.from, anySourceOf(machine, seen)].flatMap((each) => each ?? []).some((each) => known.has(observedKey(seen, each)))) continue;
628
+ if (!found.has(key)) found.set(key, fuzzEdge(seen, from));
629
+ }
630
+ return [...found.values()];
631
+ }
632
+ function byId(left, right) {
633
+ if (left.id < right.id) return -1;
634
+ return left.id > right.id ? 1 : 0;
635
+ }
636
+ /**
637
+ * Viz machine with fuzz results written in: coverage from replaying the
638
+ * shrunk corpus, newly discovered edges (origin `fuzz`), and the corpus itself.
639
+ */
640
+ function fuzzedMachine(target, result) {
641
+ const traces = result.corpus.map((entry) => execute(target, entry.steps));
642
+ const fresh = target.create().actor;
643
+ const internal = new Set(eventCatalog(fresh).internal);
644
+ const base = toVizMachine({
645
+ id: target.id,
646
+ actor: fresh
647
+ });
648
+ const added = discoveredEdges(base, traces);
649
+ const known = new Set(base.events.map((event) => event.id));
650
+ const events = [...base.events, ...[...new Set(added.map((edge) => edge.event))].filter((id) => !known.has(id)).map((id) => ({
651
+ id,
652
+ internal: internal.has(id),
653
+ payloadType: null
654
+ }))].sort(byId);
655
+ return {
656
+ ...base,
657
+ events,
658
+ states: base.states.map((state) => ({
659
+ ...state,
660
+ coverage: { hits: stateVisits(state.id, traces) }
661
+ })),
662
+ edges: [...base.edges, ...added].sort(byId).map((edge) => {
663
+ const hits = edgeHits(edge, traces);
664
+ return {
665
+ ...edge,
666
+ coverage: hits === null ? null : { hits }
667
+ };
668
+ }),
669
+ fuzz: {
670
+ seed: result.seed,
671
+ iterations: result.iterations,
672
+ corpus: result.corpus.map((entry) => ({
673
+ steps: entry.steps,
674
+ discovered: entry.discovered
675
+ })),
676
+ errors: result.errors
677
+ }
678
+ };
679
+ }
680
+ // src/fuzz/guided.ts
681
+ /**
682
+ * Literal seeding: every literal the handlers compare against, plus its
683
+ * neighbours (`L - 1`, `L + 1`) so boundary sides are both reachable.
684
+ */
685
+ function literalPool(machine) {
686
+ const numbers = /* @__PURE__ */ new Set();
687
+ const strings = /* @__PURE__ */ new Set();
688
+ for (const edge of machine.edges) {
689
+ if (edge.explanation?.status !== "analyzed") continue;
690
+ for (const literal of edge.explanation.literals) if (typeof literal === "string") strings.add(literal);
691
+ else for (const near of [
692
+ literal - 1,
693
+ literal,
694
+ literal + 1
695
+ ]) numbers.add(near);
696
+ }
697
+ return {
698
+ numbers: [...numbers].sort((left, right) => left - right),
699
+ strings: [...strings].sort()
700
+ };
701
+ }
702
+ function probesOf(machine) {
703
+ const seen = /* @__PURE__ */ new Set();
704
+ const probes = [];
705
+ for (const edge of machine.edges) {
706
+ const explanation = edge.explanation;
707
+ if (explanation?.status !== "analyzed") continue;
708
+ const state = explanation.registration === "onAny" ? null : edge.from;
709
+ for (const comparison of explanation.comparisons) {
710
+ const key = `${state}>${edge.event}>${JSON.stringify(comparison)}`;
711
+ if (seen.has(key)) continue;
712
+ seen.add(key);
713
+ probes.push({
714
+ state,
715
+ event: edge.event,
716
+ comparison
717
+ });
718
+ }
719
+ }
720
+ return probes;
721
+ }
722
+ function valueAt(payload, path) {
723
+ let current = payload;
724
+ for (const key of path.split(".").filter(Boolean)) {
725
+ if (typeof current !== "object" || current === null) return void 0;
726
+ current = Reflect.get(current, key);
727
+ }
728
+ return current;
729
+ }
730
+ function stringDistance(left, right) {
731
+ let distance = Math.abs(left.length - right.length);
732
+ for (let index = 0; index < Math.min(left.length, right.length); index += 1) if (left[index] !== right[index]) distance += 1;
733
+ return distance;
734
+ }
735
+ function numericDistance(operator, gap) {
736
+ return {
737
+ "<": [Math.max(0, gap + 1), Math.max(0, -gap)],
738
+ "<=": [Math.max(0, gap), Math.max(0, 1 - gap)],
739
+ ">": [Math.max(0, 1 - gap), Math.max(0, gap)],
740
+ ">=": [Math.max(0, -gap), Math.max(0, gap + 1)],
741
+ "===": [Math.abs(gap), gap === 0 ? 1 : 0],
742
+ "==": [Math.abs(gap), gap === 0 ? 1 : 0],
743
+ "!==": [gap === 0 ? 1 : 0, Math.abs(gap)],
744
+ "!=": [gap === 0 ? 1 : 0, Math.abs(gap)]
745
+ }[operator];
746
+ }
747
+ const EQUALITY = /* @__PURE__ */ new Set(["===", "=="]);
748
+ const INEQUALITY = /* @__PURE__ */ new Set(["!==", "!="]);
749
+ /**
750
+ * Branch distance (Korel/Tracey): how far `value` is from making the
751
+ * comparison `outcome`. 0 = that side is taken. Undefined when incomparable.
752
+ */
753
+ function branchDistance(comparison, context) {
754
+ const { value, outcome } = context;
755
+ const { literal, operator } = comparison;
756
+ if (typeof value === "number" && typeof literal === "number") {
757
+ const [whenTrue, whenFalse] = numericDistance(operator, value - literal);
758
+ return outcome ? whenTrue : whenFalse;
759
+ }
760
+ if (typeof value !== "string" || typeof literal !== "string") return void 0;
761
+ if (!EQUALITY.has(operator) && !INEQUALITY.has(operator)) return void 0;
762
+ const distance = stringDistance(value, literal);
763
+ if (EQUALITY.has(operator) === outcome) return distance;
764
+ return distance === 0 ? 1 : 0;
765
+ }
766
+ function applies(probe, observation) {
767
+ return probe.event === observation.event && (probe.state === null || probe.state === observation.from);
768
+ }
769
+ /**
770
+ * Stateful guide for `fuzz({ guide })`. For every comparison outcome not yet
771
+ * reached it remembers the smallest distance seen; a run that gets closer is
772
+ * kept as a stepping stone. Reached outcomes stop counting.
773
+ */
774
+ function branchDistanceGuide(machine) {
775
+ const probes = probesOf(machine);
776
+ const best = /* @__PURE__ */ new Map();
777
+ return (trace) => {
778
+ let improved = false;
779
+ for (const observation of trace.observations) probes.forEach((probe, index) => {
780
+ if (!applies(probe, observation)) return;
781
+ const value = valueAt(probe.comparison.source === "payload" ? observation.payload : observation.context, probe.comparison.path);
782
+ for (const outcome of [true, false]) {
783
+ const key = `${index}:${outcome}`;
784
+ const distance = branchDistance(probe.comparison, {
785
+ value,
786
+ outcome
787
+ });
788
+ const previous = best.get(key) ?? Number.POSITIVE_INFINITY;
789
+ if (distance === void 0 || previous === 0 || distance >= previous) continue;
790
+ best.set(key, distance);
791
+ improved = true;
792
+ }
793
+ });
794
+ return improved;
795
+ };
796
+ }
797
+ /** Phase 5 search options for an AST-annotated machine: literal seeding + branch distance. */
798
+ function guidance(machine) {
799
+ return {
800
+ literals: literalPool(machine),
801
+ guide: branchDistanceGuide(machine)
802
+ };
803
+ }
804
+ // src/ast/discover.ts
805
+ const ACTOR_KEYS = [
806
+ "send",
807
+ "snapshot",
808
+ "inject",
809
+ "settled"
810
+ ];
811
+ const CLOCK_KEYS = [
812
+ "setTimeout",
813
+ "clearTimeout",
814
+ "now"
815
+ ];
816
+ function hasAll(type, keys) {
817
+ return keys.every((key) => type.getProperty(key) !== void 0);
818
+ }
819
+ function unwrapOf(checker, type, node) {
820
+ if (hasAll(type, ACTOR_KEYS)) return "self";
821
+ const actor = type.getProperty("actor");
822
+ if (actor === void 0) return null;
823
+ return hasAll(checker.getTypeOfSymbolAtLocation(actor, node), ACTOR_KEYS) ? "actor" : null;
824
+ }
825
+ function isRequired(parameter) {
826
+ const declaration = parameter.valueDeclaration;
827
+ return declaration !== void 0 && ts.isParameter(declaration) && declaration.questionToken === void 0 && declaration.initializer === void 0 && declaration.dotDotDotToken === void 0;
828
+ }
829
+ /** How to call a factory so its actor runs on a clock we control, or null when it cannot be called bare. */
830
+ function callOf(checker, signature, node) {
831
+ const [first, ...rest] = signature.parameters;
832
+ if (rest.some(isRequired)) return null;
833
+ if (first === void 0) return "call";
834
+ const type = checker.getNonNullableType(checker.getTypeOfSymbolAtLocation(first, node));
835
+ if (hasAll(type, CLOCK_KEYS)) return "clock";
836
+ const clock = type.getProperty("clock");
837
+ if (clock !== void 0) {
838
+ if (hasAll(checker.getNonNullableType(checker.getTypeOfSymbolAtLocation(clock, node)), CLOCK_KEYS)) return "options";
839
+ }
840
+ return isRequired(first) ? null : "call";
841
+ }
842
+ function machineExport(checker, symbol, node) {
843
+ const target = symbol.getFlags() & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol;
844
+ const type = checker.getTypeOfSymbolAtLocation(target, node);
845
+ const name = symbol.getName();
846
+ const self = unwrapOf(checker, type, node);
847
+ if (self !== null) return {
848
+ name,
849
+ call: "instance",
850
+ unwrap: self
851
+ };
852
+ for (const signature of checker.getSignaturesOfType(type, ts.SignatureKind.Call)) {
853
+ const unwrap = unwrapOf(checker, signature.getReturnType(), node);
854
+ const call = unwrap === null ? null : callOf(checker, signature, node);
855
+ if (unwrap !== null && call !== null) return {
856
+ name,
857
+ call,
858
+ unwrap
859
+ };
860
+ }
861
+ return null;
862
+ }
863
+ /** Exports of the file that are actors or build one, in source order. */
864
+ function machineExports(loaded) {
865
+ const { checker, sourceFile } = loaded;
866
+ const module = checker.getSymbolAtLocation(sourceFile);
867
+ if (module === void 0) return [];
868
+ return checker.getExportsOfModule(module).flatMap((symbol) => {
869
+ const found = machineExport(checker, symbol, sourceFile);
870
+ return found === null ? [] : [{
871
+ found,
872
+ at: symbol.declarations?.[0]?.getStart() ?? 0
873
+ }];
874
+ }).sort((left, right) => left.at - right.at).map(({ found }) => found);
875
+ }
876
+ // src/ast/program.ts
877
+ function compilerOptions(source) {
878
+ const configPath = source.tsconfig ?? ts.findConfigFile(dirname(source.file), (path) => ts.sys.fileExists(path));
879
+ if (configPath === void 0) return {
880
+ strict: true,
881
+ allowImportingTsExtensions: true,
882
+ noEmit: true
883
+ };
884
+ const read = ts.readConfigFile(configPath, (path) => ts.sys.readFile(path));
885
+ return ts.parseJsonConfigFileContent(read.config, ts.sys, dirname(configPath)).options;
886
+ }
887
+ /** Literal string type of `expr.<property>` (e.g. a StateRef's `name`), or null. */
888
+ function literalProperty(checker, expr, property) {
889
+ const symbol = checker.getTypeAtLocation(expr).getProperty(property);
890
+ if (symbol === void 0) return null;
891
+ const type = checker.getTypeOfSymbolAtLocation(symbol, expr);
892
+ return type.isStringLiteral() ? type.value : null;
893
+ }
894
+ function property(literal, name) {
895
+ for (const element of literal.properties) {
896
+ if (!element.name || !ts.isIdentifier(element.name) || element.name.text !== name) continue;
897
+ if (ts.isPropertyAssignment(element)) return element.initializer;
898
+ if (ts.isShorthandPropertyAssignment(element)) return element.name;
899
+ if (ts.isMethodDeclaration(element)) return void 0;
900
+ }
901
+ }
902
+ function toRegistration(checker, call) {
903
+ if (!ts.isPropertyAccessExpression(call.expression)) return null;
904
+ const method = call.expression.name.text;
905
+ if (method !== "on" && method !== "onAny") return null;
906
+ const options = call.arguments[method === "on" ? 1 : 0];
907
+ if (options === void 0 || !ts.isObjectLiteralExpression(options)) return null;
908
+ const eventRef = property(options, "event");
909
+ if (eventRef === void 0) return null;
910
+ const stateArg = method === "on" ? call.arguments[0] : void 0;
911
+ return {
912
+ kind: method,
913
+ state: stateArg === void 0 ? null : literalProperty(checker, stateArg, "name"),
914
+ event: literalProperty(checker, eventRef, "type"),
915
+ eventRef,
916
+ handler: property(options, "handler"),
917
+ call
918
+ };
919
+ }
920
+ function locationOf(node, root) {
921
+ const sourceFile = node.getSourceFile();
922
+ const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
923
+ return {
924
+ file: relative(root, sourceFile.fileName).split(sep).join("/"),
925
+ line: position.line + 1,
926
+ column: position.character + 1
927
+ };
928
+ }
929
+ /**
930
+ * Type-check one file and collect its handler registrations. Any compiler
931
+ * failure becomes a Left reason; it never throws.
932
+ */
933
+ function loadSource(source) {
934
+ const loaded = Either.from(() => {
935
+ const program = ts.createProgram([source.file], compilerOptions(source));
936
+ const sourceFile = program.getSourceFile(source.file);
937
+ if (sourceFile === void 0) return `source not found: ${source.file}`;
938
+ const checker = program.getTypeChecker();
939
+ const registrations = [];
940
+ const visit = (node) => {
941
+ const found = ts.isCallExpression(node) ? toRegistration(checker, node) : null;
942
+ if (found) registrations.push(found);
943
+ ts.forEachChild(node, visit);
944
+ };
945
+ visit(sourceFile);
946
+ return {
947
+ checker,
948
+ sourceFile,
949
+ root: source.root,
950
+ registrations
951
+ };
952
+ });
953
+ if (Either.isLeft(loaded)) return Either.left(`compiler failed: ${String(loaded[0])}`);
954
+ const value = loaded[1];
955
+ return typeof value === "string" ? Either.left(value) : Either.right(value);
956
+ }
957
+ // src/ast/reads.ts
958
+ const COMPARISONS = /* @__PURE__ */ new Map([
959
+ [ts.SyntaxKind.LessThanToken, "<"],
960
+ [ts.SyntaxKind.LessThanEqualsToken, "<="],
961
+ [ts.SyntaxKind.GreaterThanToken, ">"],
962
+ [ts.SyntaxKind.GreaterThanEqualsToken, ">="],
963
+ [ts.SyntaxKind.EqualsEqualsEqualsToken, "==="],
964
+ [ts.SyntaxKind.ExclamationEqualsEqualsToken, "!=="],
965
+ [ts.SyntaxKind.EqualsEqualsToken, "=="],
966
+ [ts.SyntaxKind.ExclamationEqualsToken, "!="]
967
+ ]);
968
+ const FLIPPED = {
969
+ "<": ">",
970
+ "<=": ">=",
971
+ ">": "<",
972
+ ">=": "<=",
973
+ "===": "===",
974
+ "!==": "!==",
975
+ "==": "==",
976
+ "!=": "!="
977
+ };
978
+ function unwrap(expr) {
979
+ if (ts.isParenthesizedExpression(expr) || ts.isNonNullExpression(expr)) return unwrap(expr.expression);
980
+ if (ts.isBinaryExpression(expr) && expr.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken) return unwrap(expr.left);
981
+ return expr;
982
+ }
983
+ /** `a.b.c` -> root `a`, names `[b, c]`. */
984
+ function chainOf(expr) {
985
+ const names = [];
986
+ let current = unwrap(expr);
987
+ while (ts.isPropertyAccessExpression(current)) {
988
+ names.unshift(current.name.text);
989
+ current = unwrap(current.expression);
990
+ }
991
+ return {
992
+ root: current,
993
+ names
994
+ };
995
+ }
996
+ function isContextGet(expr, scope) {
997
+ if (!ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression)) return false;
998
+ const target = expr.expression;
999
+ return target.name.text === "get" && ts.isIdentifier(target.expression) && target.expression.text === scope.contextName;
1000
+ }
1001
+ function join$1(parts) {
1002
+ return parts.filter(Boolean).join(".");
1003
+ }
1004
+ /** Where an expression's value comes from: the event payload, the context, or neither. */
1005
+ function classify(expr, scope) {
1006
+ const { root, names } = chainOf(expr);
1007
+ if (isContextGet(root, scope)) return {
1008
+ source: "context",
1009
+ path: join$1(names)
1010
+ };
1011
+ if (!ts.isIdentifier(root)) return null;
1012
+ if (root.text === scope.eventName && names[0] === "payload") return {
1013
+ source: "payload",
1014
+ path: join$1(names.slice(1))
1015
+ };
1016
+ const alias = scope.payloadAliases.get(root.text);
1017
+ if (alias !== void 0) return {
1018
+ source: "payload",
1019
+ path: join$1([alias, ...names])
1020
+ };
1021
+ if (scope.contextAliases.has(root.text)) return {
1022
+ source: "context",
1023
+ path: join$1(names)
1024
+ };
1025
+ return null;
1026
+ }
1027
+ function literalValue(expr) {
1028
+ const node = unwrap(expr);
1029
+ if (ts.isNumericLiteral(node)) return Number(node.text);
1030
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text;
1031
+ if (node.kind === ts.SyntaxKind.TrueKeyword) return true;
1032
+ if (node.kind === ts.SyntaxKind.FalseKeyword) return false;
1033
+ if (ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.MinusToken) {
1034
+ const inner = literalValue(node.operand);
1035
+ return typeof inner === "number" ? -inner : void 0;
1036
+ }
1037
+ }
1038
+ function comparison(node, scope) {
1039
+ const operator = COMPARISONS.get(node.operatorToken.kind);
1040
+ if (operator === void 0) return null;
1041
+ const rightLiteral = literalValue(node.right);
1042
+ const leftLiteral = literalValue(node.left);
1043
+ if (rightLiteral !== void 0) {
1044
+ const side = classify(node.left, scope);
1045
+ return side ? {
1046
+ ...side,
1047
+ operator,
1048
+ literal: rightLiteral
1049
+ } : null;
1050
+ }
1051
+ if (leftLiteral === void 0) return null;
1052
+ const side = classify(node.right, scope);
1053
+ return side ? {
1054
+ ...side,
1055
+ operator: FLIPPED[operator],
1056
+ literal: leftLiteral
1057
+ } : null;
1058
+ }
1059
+ /** Outermost access in a chain, minus a trailing method name when it is called. */
1060
+ function readPath(node) {
1061
+ const parent = node.parent;
1062
+ if (ts.isPropertyAccessExpression(parent) && parent.expression === node) return null;
1063
+ if (ts.isCallExpression(parent) && parent.expression === node && ts.isPropertyAccessExpression(node)) return node.expression;
1064
+ return node;
1065
+ }
1066
+ function isAssignmentTarget(node) {
1067
+ const parent = node.parent;
1068
+ return ts.isBinaryExpression(parent) && parent.left === node && parent.operatorToken.kind === ts.SyntaxKind.EqualsToken;
1069
+ }
1070
+ function recordAccess(node, context) {
1071
+ const target = readPath(node);
1072
+ if (target === null) return;
1073
+ const source = classify(target, context.scope);
1074
+ if (source === null) {
1075
+ const { root, names } = chainOf(target);
1076
+ if (ts.isIdentifier(root) && names[0] === "state" && root.text !== context.scope.eventName) context.reads.readsActorState = true;
1077
+ return;
1078
+ }
1079
+ if (ts.isVariableDeclaration(target.parent) && !ts.isIdentifier(target.parent.name)) return;
1080
+ const path = source.path || "*";
1081
+ if (source.source === "payload") context.reads.payloadReads.add(path);
1082
+ else if (isAssignmentTarget(node)) context.reads.contextWrites.add(path);
1083
+ else if (path !== "*") context.reads.contextReads.add(path);
1084
+ }
1085
+ /** Walk a handler body collecting payload/context reads, context writes and comparisons. */
1086
+ function collectReads(body, scope) {
1087
+ const reads = {
1088
+ payloadReads: /* @__PURE__ */ new Set(),
1089
+ contextReads: /* @__PURE__ */ new Set(),
1090
+ contextWrites: /* @__PURE__ */ new Set(),
1091
+ readsActorState: false,
1092
+ comparisons: []
1093
+ };
1094
+ const visit = (node) => {
1095
+ if (ts.isPropertyAccessExpression(node) || ts.isIdentifier(node)) {
1096
+ if (!ts.isPropertyAccessExpression(node.parent) || node.parent.expression === node) recordAccess(node, {
1097
+ scope,
1098
+ reads
1099
+ });
1100
+ }
1101
+ if (ts.isBinaryExpression(node)) {
1102
+ const found = comparison(node, scope);
1103
+ if (found) reads.comparisons.push(found);
1104
+ }
1105
+ ts.forEachChild(node, visit);
1106
+ };
1107
+ visit(body);
1108
+ return reads;
1109
+ }
1110
+ // src/ast/handler.ts
1111
+ const SNIPPET_LINES = 4;
1112
+ function text(node) {
1113
+ return node.getText().replace(/\s+/g, " ");
1114
+ }
1115
+ function snippet(node) {
1116
+ return node.getText().split("\n").slice(0, SNIPPET_LINES).join("\n");
1117
+ }
1118
+ function negate(condition) {
1119
+ return `!(${condition})`;
1120
+ }
1121
+ function stateTarget(checker, expr) {
1122
+ const name = literalProperty(checker, expr, "name");
1123
+ return name === null ? {
1124
+ kind: "unresolved",
1125
+ text: text(expr)
1126
+ } : {
1127
+ kind: "state",
1128
+ state: name
1129
+ };
1130
+ }
1131
+ function stateProperty(literal) {
1132
+ for (const element of literal.properties) {
1133
+ if (!element.name || !ts.isIdentifier(element.name) || element.name.text !== "state") continue;
1134
+ if (ts.isPropertyAssignment(element)) return element.initializer;
1135
+ if (ts.isShorthandPropertyAssignment(element)) return element.name;
1136
+ }
1137
+ }
1138
+ /** Split a returned expression into branches: ternaries fork, object literals end a path. */
1139
+ function branchesOf(expr, at) {
1140
+ const { walk, conditions } = at;
1141
+ if (expr === void 0) {
1142
+ walk.branches.push({
1143
+ conditions,
1144
+ target: { kind: "stay" }
1145
+ });
1146
+ return;
1147
+ }
1148
+ if (ts.isParenthesizedExpression(expr)) return branchesOf(expr.expression, at);
1149
+ if (ts.isConditionalExpression(expr)) {
1150
+ walk.conditions.push(expr.condition);
1151
+ const condition = text(expr.condition);
1152
+ branchesOf(expr.whenTrue, {
1153
+ walk,
1154
+ conditions: [...conditions, condition]
1155
+ });
1156
+ branchesOf(expr.whenFalse, {
1157
+ walk,
1158
+ conditions: [...conditions, negate(condition)]
1159
+ });
1160
+ return;
1161
+ }
1162
+ if (!ts.isObjectLiteralExpression(expr)) {
1163
+ walk.branches.push({
1164
+ conditions,
1165
+ target: {
1166
+ kind: "unresolved",
1167
+ text: text(expr)
1168
+ }
1169
+ });
1170
+ return;
1171
+ }
1172
+ const state = stateProperty(expr);
1173
+ if (state === void 0) {
1174
+ walk.branches.push({
1175
+ conditions,
1176
+ target: { kind: "stay" }
1177
+ });
1178
+ return;
1179
+ }
1180
+ if (ts.isConditionalExpression(state)) {
1181
+ walk.conditions.push(state.condition);
1182
+ const condition = text(state.condition);
1183
+ walk.branches.push({
1184
+ conditions: [...conditions, condition],
1185
+ target: stateTarget(walk.checker, state.whenTrue)
1186
+ });
1187
+ walk.branches.push({
1188
+ conditions: [...conditions, negate(condition)],
1189
+ target: stateTarget(walk.checker, state.whenFalse)
1190
+ });
1191
+ return;
1192
+ }
1193
+ walk.branches.push({
1194
+ conditions,
1195
+ target: stateTarget(walk.checker, state)
1196
+ });
1197
+ }
1198
+ /** Walk statements; returns true when every path through them returns. */
1199
+ function walkStatements(statements, at) {
1200
+ let guards = at.conditions;
1201
+ for (const statement of statements) {
1202
+ if (walkStatement(statement, {
1203
+ walk: at.walk,
1204
+ conditions: guards
1205
+ }) === "returns") return true;
1206
+ if (ts.isIfStatement(statement) && statement.elseStatement === void 0) {
1207
+ if (alwaysReturns(statement.thenStatement)) guards = [...guards, negate(text(statement.expression))];
1208
+ }
1209
+ }
1210
+ return false;
1211
+ }
1212
+ function alwaysReturns(statement) {
1213
+ if (ts.isReturnStatement(statement)) return true;
1214
+ if (ts.isBlock(statement)) return statement.statements.some(alwaysReturns);
1215
+ if (ts.isIfStatement(statement)) return statement.elseStatement !== void 0 && alwaysReturns(statement.thenStatement) && alwaysReturns(statement.elseStatement);
1216
+ return false;
1217
+ }
1218
+ function walkStatement(statement, at) {
1219
+ const { walk, conditions } = at;
1220
+ if (ts.isReturnStatement(statement)) {
1221
+ walk.returnsSeen += 1;
1222
+ branchesOf(statement.expression, at);
1223
+ return "returns";
1224
+ }
1225
+ if (ts.isBlock(statement)) return walkStatements(statement.statements, at) ? "returns" : "continues";
1226
+ if (!ts.isIfStatement(statement)) return "continues";
1227
+ walk.conditions.push(statement.expression);
1228
+ const condition = text(statement.expression);
1229
+ walkStatement(statement.thenStatement, {
1230
+ walk,
1231
+ conditions: [...conditions, condition]
1232
+ });
1233
+ if (statement.elseStatement) walkStatement(statement.elseStatement, {
1234
+ walk,
1235
+ conditions: [...conditions, negate(condition)]
1236
+ });
1237
+ return alwaysReturns(statement) ? "returns" : "continues";
1238
+ }
1239
+ function countReturns(body) {
1240
+ let count = 0;
1241
+ const visit = (node) => {
1242
+ if (ts.isFunctionLike(node) && node !== body) return;
1243
+ if (ts.isReturnStatement(node)) count += 1;
1244
+ ts.forEachChild(node, visit);
1245
+ };
1246
+ ts.forEachChild(body, visit);
1247
+ return count;
1248
+ }
1249
+ function bindingName(node) {
1250
+ return node !== void 0 && ts.isIdentifier(node) ? node.text : null;
1251
+ }
1252
+ function contextBinding(parameter) {
1253
+ if (parameter === void 0 || !ts.isObjectBindingPattern(parameter.name)) return null;
1254
+ for (const element of parameter.name.elements) {
1255
+ const key = element.propertyName ?? element.name;
1256
+ if (ts.isIdentifier(key) && key.text === "context") return bindingName(element.name);
1257
+ }
1258
+ return null;
1259
+ }
1260
+ /** Record locals bound to the payload (`const { a } = event.payload`) or to `context.get()`. */
1261
+ function collectAliases(body, scope) {
1262
+ const visit = (node) => {
1263
+ if (ts.isVariableDeclaration(node) && node.initializer) {
1264
+ if (ts.isIdentifier(node.name) && isContextGet(node.initializer, scope)) scope.contextAliases.add(node.name.text);
1265
+ const source = classify(node.initializer, scope);
1266
+ if (source?.source === "payload") bindPayload(node.name, {
1267
+ scope,
1268
+ path: source.path
1269
+ });
1270
+ }
1271
+ ts.forEachChild(node, visit);
1272
+ };
1273
+ visit(body);
1274
+ }
1275
+ function bindPayload(name, at) {
1276
+ if (ts.isIdentifier(name)) {
1277
+ at.scope.payloadAliases.set(name.text, at.path);
1278
+ return;
1279
+ }
1280
+ if (!ts.isObjectBindingPattern(name)) return;
1281
+ for (const element of name.elements) {
1282
+ const key = element.propertyName ?? element.name;
1283
+ if (!ts.isIdentifier(key)) continue;
1284
+ bindPayload(element.name, {
1285
+ scope: at.scope,
1286
+ path: [at.path, key.text].filter(Boolean).join(".")
1287
+ });
1288
+ }
1289
+ }
1290
+ function conditionLiterals(conditions) {
1291
+ const literals = /* @__PURE__ */ new Set();
1292
+ const visit = (node) => {
1293
+ if (ts.isExpression(node)) {
1294
+ const value = literalValue(node);
1295
+ if (typeof value === "number" || typeof value === "string") literals.add(value);
1296
+ }
1297
+ ts.forEachChild(node, visit);
1298
+ };
1299
+ for (const condition of conditions) visit(condition);
1300
+ return [...literals].sort((left, right) => String(left).localeCompare(String(right)));
1301
+ }
1302
+ function analyzeFunction(fn, at) {
1303
+ const scope = {
1304
+ eventName: bindingName(fn.parameters[0]?.name),
1305
+ contextName: contextBinding(fn.parameters[1]),
1306
+ payloadAliases: /* @__PURE__ */ new Map(),
1307
+ contextAliases: /* @__PURE__ */ new Set()
1308
+ };
1309
+ const location = locationOf(at.registration.call, at.root);
1310
+ const walk = {
1311
+ checker: at.checker,
1312
+ branches: [],
1313
+ returnsSeen: 0,
1314
+ conditions: []
1315
+ };
1316
+ collectAliases(fn.body, scope);
1317
+ if (ts.isBlock(fn.body)) {
1318
+ walkStatements(fn.body.statements, {
1319
+ walk,
1320
+ conditions: []
1321
+ });
1322
+ if (walk.returnsSeen !== countReturns(fn.body)) return {
1323
+ status: "not_analyzed",
1324
+ location,
1325
+ reason: "unsupported control flow (switch, loop or try around a return)",
1326
+ snippet: snippet(fn)
1327
+ };
1328
+ if (!alwaysReturns(fn.body)) walk.branches.push({
1329
+ conditions: [],
1330
+ target: { kind: "stay" }
1331
+ });
1332
+ } else branchesOf(fn.body, {
1333
+ walk,
1334
+ conditions: []
1335
+ });
1336
+ const reads = collectReads(fn.body, scope);
1337
+ const sorted = (values) => [...values].sort();
1338
+ return {
1339
+ status: "analyzed",
1340
+ location,
1341
+ registration: at.registration.kind,
1342
+ branches: walk.branches,
1343
+ payloadReads: sorted(reads.payloadReads),
1344
+ contextReads: sorted(reads.contextReads),
1345
+ contextWrites: sorted(reads.contextWrites),
1346
+ readsActorState: reads.readsActorState,
1347
+ comparisons: reads.comparisons,
1348
+ literals: conditionLiterals(walk.conditions)
1349
+ };
1350
+ }
1351
+ /** Explain one registration's handler. Anything not understood is `not_analyzed`, never guessed. */
1352
+ function explainRegistration(registration, context) {
1353
+ const handler = registration.handler;
1354
+ const location = locationOf(registration.call, context.root);
1355
+ if (handler === void 0) return {
1356
+ status: "not_analyzed",
1357
+ location,
1358
+ reason: "handler is not a plain property",
1359
+ snippet: snippet(registration.call)
1360
+ };
1361
+ if (ts.isArrowFunction(handler) || ts.isFunctionExpression(handler)) return analyzeFunction(handler, {
1362
+ registration,
1363
+ ...context
1364
+ });
1365
+ return {
1366
+ status: "not_analyzed",
1367
+ location,
1368
+ reason: `handler \`${text(handler)}\` is a reference, not an inline function`,
1369
+ snippet: snippet(handler)
1370
+ };
1371
+ }
1372
+ // src/ast/payloads.ts
1373
+ /** Most samples one type yields; unions and object variants stop here. */
1374
+ const MAX_SAMPLES = 8;
1375
+ /** Deepest nesting followed; deeper values become null. */
1376
+ const MAX_DEPTH = 5;
1377
+ function unique(values) {
1378
+ const seen = /* @__PURE__ */ new Set();
1379
+ return values.filter((value) => {
1380
+ const key = JSON.stringify(value);
1381
+ if (seen.has(key)) return false;
1382
+ seen.add(key);
1383
+ return true;
1384
+ }).slice(0, MAX_SAMPLES);
1385
+ }
1386
+ function primitiveSamples(type, checker) {
1387
+ if (type.isStringLiteral()) return [type.value];
1388
+ if (type.isNumberLiteral()) return [type.value];
1389
+ const flags = type.getFlags();
1390
+ if (flags & ts.TypeFlags.BooleanLiteral) return [checker.typeToString(type) === "true"];
1391
+ if (flags & ts.TypeFlags.StringLike) return ["sample"];
1392
+ if (flags & ts.TypeFlags.NumberLike) return [1];
1393
+ if (flags & ts.TypeFlags.BigIntLike) return [1];
1394
+ if (flags & ts.TypeFlags.Null) return [null];
1395
+ if (flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) return [null];
1396
+ return null;
1397
+ }
1398
+ /**
1399
+ * One value per property from its first sample, plus one variant per further
1400
+ * sample of each property: a union of literals in one field shows up as
1401
+ * separate payloads without multiplying across fields.
1402
+ */
1403
+ function objectSamples(type, walk) {
1404
+ const fields = walk.checker.getPropertiesOfType(type).flatMap((symbol) => {
1405
+ const fieldType = walk.checker.getTypeOfSymbolAtLocation(symbol, walk.node);
1406
+ if (walk.checker.getSignaturesOfType(fieldType, ts.SignatureKind.Call).length > 0) return [];
1407
+ const optional = (symbol.getFlags() & ts.SymbolFlags.Optional) !== 0;
1408
+ const samples = samplesOf(walk.checker.getNonNullableType(fieldType), {
1409
+ ...walk,
1410
+ depth: walk.depth + 1
1411
+ });
1412
+ return optional && samples.length === 0 ? [] : [{
1413
+ name: symbol.getName(),
1414
+ samples
1415
+ }];
1416
+ });
1417
+ const base = Object.fromEntries(fields.map((field) => [field.name, field.samples[0] ?? null]));
1418
+ return unique([base, ...fields.flatMap((field) => field.samples.slice(1).map((sample) => ({
1419
+ ...base,
1420
+ [field.name]: sample
1421
+ })))]);
1422
+ }
1423
+ /** JSON samples a value of `type` could take. Empty when the type has no JSON form (void, functions). */
1424
+ function samplesOf(type, walk) {
1425
+ if (walk.depth > MAX_DEPTH) return [null];
1426
+ const flags = type.getFlags();
1427
+ if (flags & (ts.TypeFlags.Void | ts.TypeFlags.Undefined | ts.TypeFlags.Never)) return [];
1428
+ if (type.isUnion()) return unique(type.types.flatMap((member) => samplesOf(member, walk)));
1429
+ const { checker } = walk;
1430
+ const primitive = primitiveSamples(type, checker);
1431
+ if (primitive !== null) return primitive;
1432
+ if (checker.isArrayType(type) || checker.isTupleType(type)) {
1433
+ const element = checker.getIndexTypeOfType(type, ts.IndexKind.Number);
1434
+ const first = element === void 0 ? void 0 : samplesOf(element, {
1435
+ ...walk,
1436
+ depth: walk.depth + 1
1437
+ })[0];
1438
+ return [first === void 0 ? [] : [first]];
1439
+ }
1440
+ if (checker.getSignaturesOfType(type, ts.SignatureKind.Call).length > 0) return [];
1441
+ return flags & ts.TypeFlags.Object || type.isIntersection() ? objectSamples(type, walk) : [null];
1442
+ }
1443
+ /** Payload type of an EventRef: the parameter of its one-argument `create` overload. */
1444
+ function payloadType(checker, eventRef) {
1445
+ const create = checker.getTypeAtLocation(eventRef).getProperty("create");
1446
+ if (create === void 0) return null;
1447
+ const parameter = checker.getSignaturesOfType(checker.getTypeOfSymbolAtLocation(create, eventRef), ts.SignatureKind.Call).find((signature) => signature.parameters.length === 1)?.parameters[0];
1448
+ return parameter === void 0 ? null : checker.getTypeOfSymbolAtLocation(parameter, eventRef);
1449
+ }
1450
+ /**
1451
+ * Sample payloads per event, read from the payload types of the events the
1452
+ * file's handlers register (`event("x")<{ ... }>()`). Events without a payload
1453
+ * are left out, as the fuzzer expects.
1454
+ */
1455
+ function payloadSamples(loaded) {
1456
+ const entries = loaded.registrations.flatMap((registration) => {
1457
+ if (registration.event === null) return [];
1458
+ const type = payloadType(loaded.checker, registration.eventRef);
1459
+ if (type === null) return [];
1460
+ const samples = samplesOf(type, {
1461
+ checker: loaded.checker,
1462
+ node: registration.eventRef,
1463
+ depth: 0
1464
+ });
1465
+ return samples.length === 0 ? [] : [[registration.event, samples]];
1466
+ });
1467
+ return Object.fromEntries(entries.sort(([left], [right]) => left.localeCompare(right)));
1468
+ }
1469
+ // src/ast/annotate.ts
1470
+ function sitesOf(loaded) {
1471
+ return loaded.registrations.flatMap((registration) => {
1472
+ if (registration.event === null) return [];
1473
+ const explanation = Either.from(() => explainRegistration(registration, {
1474
+ checker: loaded.checker,
1475
+ root: loaded.root
1476
+ }));
1477
+ return [{
1478
+ state: registration.state,
1479
+ event: registration.event,
1480
+ explanation: Either.getOrElse(explanation, { onLeft: (error) => ({
1481
+ status: "not_analyzed",
1482
+ location: null,
1483
+ reason: `analysis failed: ${String(error)}`,
1484
+ snippet: null
1485
+ }) })
1486
+ }];
1487
+ });
1488
+ }
1489
+ /** Explain every handler registration in a file. Left = the file could not be analyzed at all. */
1490
+ function analyzeSource(source) {
1491
+ return Either.chain(loadSource(source), { onRight: (loaded) => Either.right(sitesOf(loaded)) });
1492
+ }
1493
+ /** Handlers, payload samples and machine exports of a file, from one TypeScript program. */
1494
+ function analyzeModule(source) {
1495
+ return Either.chain(loadSource(source), { onRight: (loaded) => Either.right({
1496
+ sites: sitesOf(loaded),
1497
+ payloads: payloadSamples(loaded),
1498
+ machines: machineExports(loaded)
1499
+ }) });
1500
+ }
1501
+ /** Region-prefixed ids (`region.state`) match registrations by their last segment. */
1502
+ function localState(id) {
1503
+ return id.slice(id.lastIndexOf(".") + 1);
1504
+ }
1505
+ function explain(edge, context) {
1506
+ if (edge.kind === "initial" || edge.kind === "effect") return null;
1507
+ const state = localState(edge.from);
1508
+ const site = context.sites.find((candidate) => candidate.state === state && candidate.event === edge.event) ?? context.sites.find((candidate) => candidate.state === null && candidate.event === edge.event);
1509
+ if (site) return site.explanation;
1510
+ return {
1511
+ status: "not_analyzed",
1512
+ location: null,
1513
+ reason: `no m.on/m.onAny registration for (${state}, ${edge.event}) found in ${context.file}`,
1514
+ snippet: null
1515
+ };
1516
+ }
1517
+ /**
1518
+ * Fill `explanation` on handler-driven edges. Never adds or removes edges:
1519
+ * the runtime graph stays the source of truth. A source that cannot be
1520
+ * analyzed marks every handler edge `not_analyzed` with the reason.
1521
+ */
1522
+ function annotateMachine(machine, source) {
1523
+ return annotateWith(machine, {
1524
+ analysis: analyzeSource(source),
1525
+ source
1526
+ });
1527
+ }
1528
+ /** `annotateMachine` with the analysis already done (one program for many machines). */
1529
+ function annotateWith(machine, context) {
1530
+ const { analysis, source } = context;
1531
+ const file = source.file.slice(source.root.length).replace(/^[\\/]/, "");
1532
+ const edges = machine.edges.map((edge) => {
1533
+ if (Either.isLeft(analysis)) {
1534
+ const explanation = edge.kind === "transition" || edge.kind === "stay" ? {
1535
+ status: "not_analyzed",
1536
+ location: null,
1537
+ reason: analysis[0],
1538
+ snippet: null
1539
+ } : null;
1540
+ return {
1541
+ ...edge,
1542
+ explanation
1543
+ };
1544
+ }
1545
+ return {
1546
+ ...edge,
1547
+ explanation: explain(edge, {
1548
+ sites: analysis[1],
1549
+ file
1550
+ })
1551
+ };
1552
+ });
1553
+ return {
1554
+ ...machine,
1555
+ edges
1556
+ };
1557
+ }
1558
+ // src/inspect.ts
1559
+ const DEFAULT_ITERATIONS = 2e3;
1560
+ function isVirtualClock(clock) {
1561
+ return typeof Reflect.get(clock, "advance") === "function";
1562
+ }
1563
+ function warningsOf(machine) {
1564
+ const routing = routingWarnings(machine).map((warning) => warning.message);
1565
+ const sends = machine.states.flatMap((state) => unhandledSends(machine, state.id).map(({ effect, event }) => `effect ${effect} in ${state.id} sends ${event}, but ${state.id} has no handler for it`));
1566
+ return [...routing, ...sends];
1567
+ }
1568
+ /** The target as the fuzzer drives it, or why it cannot run on a virtual clock. */
1569
+ function runner(target, payloads) {
1570
+ if (!isVirtualClock(target.create().clock)) return Either.left("not fuzzed: the actor does not run on a VirtualClock");
1571
+ return Either.right({
1572
+ id: target.id,
1573
+ create: () => {
1574
+ const actor = target.create();
1575
+ return {
1576
+ actor,
1577
+ clock: isVirtualClock(actor.clock) ? actor.clock : VirtualClock()
1578
+ };
1579
+ },
1580
+ payloads
1581
+ });
1582
+ }
1583
+ function annotator(target, analysis) {
1584
+ const { source } = target;
1585
+ if (analysis === null || source === void 0) return (machine) => machine;
1586
+ const sites = Either.map(analysis, { onRight: (found) => found.sites });
1587
+ return (machine) => annotateWith(machine, {
1588
+ analysis: sites,
1589
+ source
1590
+ });
1591
+ }
1592
+ /** Fuzz and test coverage on top of the structure, as far as the target allows. */
1593
+ function driven(target, context) {
1594
+ const { structure, run } = context;
1595
+ if (Either.isLeft(run)) return {
1596
+ machine: structure,
1597
+ skipped: [run[0]]
1598
+ };
1599
+ const settings = target.fuzz;
1600
+ const fuzzed = settings === false ? structure : fuzzedMachine(run[1], fuzz(run[1], {
1601
+ seed: settings?.seed ?? DEFAULT_SEED,
1602
+ iterations: settings?.iterations ?? DEFAULT_ITERATIONS,
1603
+ ...guidance(structure)
1604
+ }));
1605
+ return {
1606
+ machine: target.tests === void 0 ? fuzzed : withTestCoverage(run[1], fuzzed, target.tests),
1607
+ skipped: []
1608
+ };
1609
+ }
1610
+ function inspectWith(target, analysis) {
1611
+ const annotate = annotator(target, analysis);
1612
+ const inferred = Either.getRight(analysis ?? Either.left("none"))?.payloads ?? {};
1613
+ const { machine, skipped } = driven(target, {
1614
+ structure: annotate(toVizMachine({
1615
+ id: target.id,
1616
+ actor: target.create()
1617
+ })),
1618
+ run: runner(target, {
1619
+ ...inferred,
1620
+ ...target.payloads
1621
+ })
1622
+ });
1623
+ const quiet = target.fuzz === false && target.tests === void 0;
1624
+ const unanalyzed = analysis === null ? [] : [Either.getLeft(analysis)].filter((each) => each !== void 0);
1625
+ const annotated = annotate(machine);
1626
+ return {
1627
+ machine: annotated,
1628
+ warnings: [
1629
+ ...unanalyzed,
1630
+ ...quiet ? [] : skipped,
1631
+ ...warningsOf(annotated)
1632
+ ]
1633
+ };
1634
+ }
1635
+ /**
1636
+ * One machine in, one viz machine out: structure, edge explanations and
1637
+ * payloads from the source's types, fuzz coverage, test coverage. Each part
1638
+ * that cannot run is skipped with a warning, never guessed.
1639
+ */
1640
+ function inspect(target) {
1641
+ return inspectWith(target, target.source === void 0 ? null : analyzeModule(target.source));
1642
+ }
1643
+ /** Directory of the nearest package.json above `file`: locations are reported relative to it. */
1644
+ function projectRoot(file) {
1645
+ let dir = dirname(file);
1646
+ while (!existsSync(join(dir, "package.json")) && dirname(dir) !== dir) dir = dirname(dir);
1647
+ return dir;
1648
+ }
1649
+ /** `createCheckoutActor` → `checkout`; other names are kept. */
1650
+ function machineId(exportName) {
1651
+ const core = exportName.replace(/^create/, "").replace(/Actor$/, "");
1652
+ return core === "" ? exportName : core.charAt(0).toLowerCase() + core.slice(1);
1653
+ }
1654
+ function isActor(value) {
1655
+ return typeof value === "object" && value !== null && [
1656
+ "send",
1657
+ "snapshot",
1658
+ "inject"
1659
+ ].every((key) => typeof Reflect.get(value, key) === "function");
1660
+ }
1661
+ /** Call an export the way its type says, and take the actor out of what it returns. */
1662
+ function factoryOf(found, value) {
1663
+ if (found.call === "instance") return () => value;
1664
+ if (typeof value !== "function") return null;
1665
+ const argument = found.call === "clock" ? () => [VirtualClock()] : found.call === "options" ? () => [{ clock: VirtualClock() }] : () => [];
1666
+ return () => Reflect.apply(value, void 0, argument());
1667
+ }
1668
+ function actorFactory(found, value) {
1669
+ const factory = factoryOf(found, value);
1670
+ if (factory === null) return null;
1671
+ const unwrap = (result) => found.unwrap === "actor" && typeof result === "object" && result !== null ? Reflect.get(result, "actor") : result;
1672
+ const sample = unwrap(factory());
1673
+ if (!isActor(sample)) return null;
1674
+ return () => {
1675
+ const actor = unwrap(factory());
1676
+ return isActor(actor) ? actor : sample;
1677
+ };
1678
+ }
1679
+ /**
1680
+ * Every machine a file exports, found from its types: actors, and functions
1681
+ * that build one (optionally taking a clock or `{ clock }`). When a file has
1682
+ * factories, plain actor exports are skipped: they are usually one of those
1683
+ * factories' results, and cannot be fuzzed.
1684
+ */
1685
+ async function inspectModule(file, options) {
1686
+ const source = {
1687
+ file,
1688
+ root: options.root ?? projectRoot(file),
1689
+ ...options.tsconfig === void 0 ? {} : { tsconfig: options.tsconfig }
1690
+ };
1691
+ const analysis = analyzeModule(source);
1692
+ if (Either.isLeft(analysis)) return {
1693
+ machines: [],
1694
+ warnings: [`${file}: ${analysis[0]}`]
1695
+ };
1696
+ const found = analysis[1].machines;
1697
+ const factories = found.filter((each) => each.call !== "instance");
1698
+ const chosen = factories.length > 0 ? factories : found;
1699
+ if (chosen.length === 0) return {
1700
+ machines: [],
1701
+ warnings: [`${file}: no exported actor or actor factory found`]
1702
+ };
1703
+ const exports = await options.load(file);
1704
+ const results = chosen.map((each) => {
1705
+ const id = machineId(each.name);
1706
+ const create = actorFactory(each, exports[each.name]);
1707
+ if (create === null) return {
1708
+ machine: null,
1709
+ warnings: [`${id}: ${each.name} did not return an actor`]
1710
+ };
1711
+ const fuzzSettings = each.call === "instance" ? false : options.fuzz ?? {};
1712
+ const inspected = inspectWith({
1713
+ id,
1714
+ create,
1715
+ source,
1716
+ fuzz: fuzzSettings
1717
+ }, analysis);
1718
+ const instance = each.call === "instance" ? ["not fuzzed: exported as one actor, not a factory"] : [];
1719
+ return {
1720
+ machine: inspected.machine,
1721
+ warnings: [...instance, ...inspected.warnings].map((warning) => `${id}: ${warning}`)
1722
+ };
1723
+ });
1724
+ return {
1725
+ machines: results.flatMap((result) => result.machine === null ? [] : [result.machine]),
1726
+ warnings: results.flatMap((result) => result.warnings)
1727
+ };
1728
+ }
1729
+ /** Machines as a document, ready for `serializeViz`. */
1730
+ function vizDocument(machines) {
1731
+ return {
1732
+ schemaVersion: VIZ_SCHEMA_VERSION,
1733
+ machines: [...machines]
1734
+ };
1735
+ }
1736
+ // src/load.ts
1737
+ const ENTRY = "virtual:mantaq-inspect";
1738
+ const RESOLVED = `\0${ENTRY}`;
1739
+ /**
1740
+ * A module that imports the machine file and the inspector side by side. Run
1741
+ * through one module graph, both see the same `@mantaq/core` instance: core
1742
+ * recognizes its state refs by a module-level registry, so a machine loaded
1743
+ * through a bundler and inspected from plain Node would look stateless.
1744
+ */
1745
+ function entrySource(file) {
1746
+ const inspect = fileURLToPath(import.meta.resolve("@mantaq/inspect"));
1747
+ return [
1748
+ `import * as machines from ${JSON.stringify(file)};`,
1749
+ `import { inspectModule } from ${JSON.stringify(inspect)};`,
1750
+ `export function run(options) {`,
1751
+ ` return inspectModule(${JSON.stringify(file)}, { ...options, load: async () => machines });`,
1752
+ `}`
1753
+ ].join("\n");
1754
+ }
1755
+ /** Vite plugin serving the entry for each `virtual:mantaq-inspect?file=<path>` id. */
1756
+ function inspectEntry() {
1757
+ return {
1758
+ name: "mantaq-inspect-entry",
1759
+ resolveId: (id) => id.startsWith(ENTRY) ? `\0${id}` : null,
1760
+ load: (id) => {
1761
+ if (!id.startsWith(RESOLVED)) return null;
1762
+ const file = new URLSearchParams(id.slice(id.indexOf("?") + 1)).get("file");
1763
+ return file === null ? null : entrySource(file);
1764
+ }
1765
+ };
1766
+ }
1767
+ function entryId(file) {
1768
+ return `${ENTRY}?${new URLSearchParams({ file }).toString()}`;
1769
+ }
1770
+ /** The `run` export of an entry module, checked: module objects are untyped. */
1771
+ function runOf(module) {
1772
+ const run = typeof module === "object" && module !== null ? Reflect.get(module, "run") : void 0;
1773
+ if (typeof run !== "function") return null;
1774
+ return async (options) => Reflect.apply(run, void 0, [options]);
1775
+ }
1776
+ /** Load `file` with Vite (TS, tsconfig paths, extensionless imports) and inspect its machines. */
1777
+ async function inspectFile(file, options = {}) {
1778
+ const { module } = await runnerImport(entryId(file), {
1779
+ configFile: false,
1780
+ logLevel: "silent",
1781
+ plugins: [inspectEntry()]
1782
+ });
1783
+ const run = runOf(module);
1784
+ if (run === null) return {
1785
+ machines: [],
1786
+ warnings: [`${file}: could not load the inspector`]
1787
+ };
1788
+ return run(options);
1789
+ }
1790
+ export { toVizMachine as C, toViz as S, withTestCoverage as _, inspect as a, eventCatalog as b, vizDocument as c, annotateMachine as d, branchDistance as f, fuzzedMachine as g, literalPool as h, runOf as i, analyzeModule as l, guidance as m, inspectEntry as n, inspectModule as o, branchDistanceGuide as p, inspectFile as r, machineId as s, entryId as t, analyzeSource as u, fuzz as v, execute as x, shrink as y };