@mooloo-ai/eutaxy 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +16 -2
  3. package/dist/aws.d.mts +15464 -0
  4. package/dist/aws.d.mts.map +1 -0
  5. package/dist/aws.mjs +24057 -0
  6. package/dist/aws.mjs.map +1 -0
  7. package/dist/bin.d.mts +1 -0
  8. package/dist/bin.mjs +17 -0
  9. package/dist/bin.mjs.map +1 -0
  10. package/dist/checksum-DhS6ofSm.mjs +6049 -0
  11. package/dist/checksum-DhS6ofSm.mjs.map +1 -0
  12. package/dist/cli-DnPkIL8h.mjs +2 -0
  13. package/dist/cli.d.mts +8 -0
  14. package/dist/cli.d.mts.map +1 -0
  15. package/dist/cli.mjs +2 -0
  16. package/dist/cloudflare.d.mts +141 -0
  17. package/dist/cloudflare.d.mts.map +1 -0
  18. package/dist/cloudflare.mjs +847 -0
  19. package/dist/cloudflare.mjs.map +1 -0
  20. package/dist/es5-DtZuPEgt.mjs +1721 -0
  21. package/dist/es5-DtZuPEgt.mjs.map +1 -0
  22. package/dist/event-streams-BANyO9xt.mjs +801 -0
  23. package/dist/event-streams-BANyO9xt.mjs.map +1 -0
  24. package/dist/explore-C5fDFOoY.d.mts +63 -0
  25. package/dist/explore-C5fDFOoY.d.mts.map +1 -0
  26. package/dist/index.d.mts +24 -0
  27. package/dist/index.d.mts.map +1 -0
  28. package/dist/index.mjs +5 -0
  29. package/dist/platform-CGVkTa16.d.mts +267 -0
  30. package/dist/platform-CGVkTa16.d.mts.map +1 -0
  31. package/dist/rolldown-runtime-C1I3wEg2.mjs +43 -0
  32. package/dist/src-B66we2mG.mjs +311 -0
  33. package/dist/src-B66we2mG.mjs.map +1 -0
  34. package/dist/src-BP6Iqc_9.mjs +117 -0
  35. package/dist/src-BP6Iqc_9.mjs.map +1 -0
  36. package/dist/src-BrCsF5RZ.mjs +1286 -0
  37. package/dist/src-BrCsF5RZ.mjs.map +1 -0
  38. package/dist/src-s6uYyC0B.mjs +89 -0
  39. package/dist/src-s6uYyC0B.mjs.map +1 -0
  40. package/dist/verdict-evEM5Gxx.mjs +12 -0
  41. package/dist/verdict-evEM5Gxx.mjs.map +1 -0
  42. package/package.json +76 -3
@@ -0,0 +1,1286 @@
1
+ import { i as canonicalJson, n as digestLog, r as emptyLog, t as appendEffect } from "./src-s6uYyC0B.mjs";
2
+ // packages/core/src/prng.ts
3
+ function rotl(x, k) {
4
+ return (x << k | x >>> 32 - k) >>> 0;
5
+ }
6
+ /** splitmix32: expands a 32-bit seed into well-mixed words for seeding. */
7
+ function splitmix32(seed) {
8
+ let a = seed >>> 0;
9
+ return () => {
10
+ a = a + 2654435769 >>> 0;
11
+ let t = a;
12
+ t = Math.imul(t ^ t >>> 16, 569420461);
13
+ t = Math.imul(t ^ t >>> 15, 1935289751);
14
+ return (t ^ t >>> 15) >>> 0;
15
+ };
16
+ }
17
+ /** FNV-1a over UTF-16 code units, so string seeds are stable across runtimes. */
18
+ function hashString(s) {
19
+ let h = 2166136261;
20
+ for (let i = 0; i < s.length; i++) {
21
+ h ^= s.charCodeAt(i);
22
+ h = Math.imul(h, 16777619);
23
+ }
24
+ return h >>> 0;
25
+ }
26
+ /** Derive an initial state from a number (taken modulo 2^32) or a string. */
27
+ function seedPrng(seed) {
28
+ const next = splitmix32(typeof seed === "string" ? hashString(seed) : seed >>> 0);
29
+ const s = [
30
+ next(),
31
+ next(),
32
+ next(),
33
+ next()
34
+ ];
35
+ if (s[0] === 0 && s[1] === 0 && s[2] === 0 && s[3] === 0) s[0] = 1;
36
+ return s;
37
+ }
38
+ /** Next 32-bit unsigned integer and the state that follows it. */
39
+ function nextU32(state) {
40
+ const [s0, s1, s2, s3] = state;
41
+ const result = Math.imul(rotl(Math.imul(s1, 5) >>> 0, 7), 9) >>> 0;
42
+ const t = s1 << 9 >>> 0;
43
+ let n2 = (s2 ^ s0) >>> 0;
44
+ let n3 = (s3 ^ s1) >>> 0;
45
+ const n1 = (s1 ^ n2) >>> 0;
46
+ const n0 = (s0 ^ n3) >>> 0;
47
+ n2 = (n2 ^ t) >>> 0;
48
+ n3 = rotl(n3, 11);
49
+ return [result, [
50
+ n0,
51
+ n1,
52
+ n2,
53
+ n3
54
+ ]];
55
+ }
56
+ /** Next float in [0, 1) with 24 bits of precision. */
57
+ function nextFloat(state) {
58
+ const [u, next] = nextU32(state);
59
+ return [(u >>> 8) * 2 ** -24, next];
60
+ }
61
+ /**
62
+ * Next integer in [0, bound). Uses rejection sampling, so every value is
63
+ * equally likely; a bound of 1 always yields 0 without consuming state.
64
+ */
65
+ function nextInt(state, bound) {
66
+ if (!Number.isInteger(bound) || bound < 1 || bound > 4294967296) throw new RangeError(`nextInt bound must be an integer in [1, 2^32], got ${bound}`);
67
+ if (bound === 1) return [0, state];
68
+ const limit = 4294967296 - 4294967296 % bound;
69
+ let s = state;
70
+ for (;;) {
71
+ const [u, next] = nextU32(s);
72
+ s = next;
73
+ if (u < limit) return [u % bound, s];
74
+ }
75
+ }
76
+ // packages/core/src/faults.ts
77
+ /**
78
+ * The fault model and the schedule. A schedule is the complete, replayable
79
+ * record of which fault was injected at which effect point in one run.
80
+ */
81
+ /**
82
+ * Decide whether to inject a fault at an effect point during exploration.
83
+ * Pure: the same state and inputs always give the same decision. The PRNG is
84
+ * only consumed when a fault is possible, so adding an ineligible point does
85
+ * not perturb later decisions.
86
+ */
87
+ function decideFault(state, allowed, faultsSoFar, policy) {
88
+ if (allowed.length === 0 || faultsSoFar >= policy.maxFaultsPerRun) return [null, state];
89
+ const [p, s1] = nextFloat(state);
90
+ if (p >= policy.faultProbability) return [null, s1];
91
+ const [i, s2] = nextInt(s1, allowed.length);
92
+ return [allowed[i] ?? null, s2];
93
+ }
94
+ // packages/core/src/invariants.ts
95
+ /**
96
+ * Invariants are plain data describing a property of the effect log; checkers
97
+ * are pure functions from (log, invariant) to a violation or null. The
98
+ * library starts with what the first planted bug needs and grows from there.
99
+ */
100
+ /** Re-derive `expected` for per-message invariants from the bodies that remain. */
101
+ function recomputePerMessageExpected(invariants, bodies) {
102
+ return invariants.map((inv) => {
103
+ if (inv.kind !== "atLeastOnce" || !inv.perMessage) return inv;
104
+ const bodyKey = inv.perMessage.bodyKey;
105
+ const expected = [];
106
+ for (const b of bodies) {
107
+ const v = getPath(b, bodyKey);
108
+ if (v !== void 0) expected.push(v);
109
+ }
110
+ return {
111
+ ...inv,
112
+ expected
113
+ };
114
+ });
115
+ }
116
+ /** The tables in the last snapshot recorded for a target, or undefined when none was. */
117
+ function lastSnapshot(log, target) {
118
+ for (let i = log.entries.length - 1; i >= 0; i--) {
119
+ const e = log.entries[i];
120
+ if (e.kind === "snapshot" && e.target === target) {
121
+ const tables = getPath(e.payload, "tables");
122
+ if (tables && typeof tables === "object" && !Array.isArray(tables)) return tables;
123
+ return {};
124
+ }
125
+ }
126
+ }
127
+ function rowMatches(row, condition) {
128
+ const v = row[condition.column];
129
+ switch (condition.op) {
130
+ case "eq": return v !== void 0 && jsonEqual(v, condition.value);
131
+ case "neq": return v === void 0 || !jsonEqual(v, condition.value);
132
+ case "gt": return typeof v === "number" && v > condition.value;
133
+ case "gte": return typeof v === "number" && v >= condition.value;
134
+ case "lt": return typeof v === "number" && v < condition.value;
135
+ case "lte": return typeof v === "number" && v <= condition.value;
136
+ case "in": return v !== void 0 && condition.value.some((c) => jsonEqual(v, c));
137
+ case "notNull": return v !== void 0 && v !== null;
138
+ }
139
+ }
140
+ /** Rows a state query selects from the log's last snapshot; undefined when there is no snapshot. */
141
+ function queryRows(log, query) {
142
+ const tables = lastSnapshot(log, query.target);
143
+ if (!tables) return void 0;
144
+ const rows = tables[query.table] ?? [];
145
+ const where = query.where ?? [];
146
+ return rows.filter((r) => where.every((c) => rowMatches(r, c)));
147
+ }
148
+ function snapshotSeq(log, target) {
149
+ let last;
150
+ for (const e of log.entries) if (e.kind === "snapshot" && e.target === target) last = e.seq;
151
+ return last === void 0 ? [] : [last];
152
+ }
153
+ /** True when two violations are of the same invariant for the same keys, whatever the traces. */
154
+ function sameViolation(a, b) {
155
+ return a.invariant === b.invariant && a.keys.length === b.keys.length && a.keys.every((k, i) => k === b.keys[i]);
156
+ }
157
+ /** Read a dot path such as "args.params.0" out of JSON data; undefined when absent. */
158
+ function getPath(value, path) {
159
+ let cur = value;
160
+ if (path === "") return cur;
161
+ for (const part of path.split(".")) {
162
+ if (cur === null || typeof cur !== "object") return void 0;
163
+ if (Array.isArray(cur)) {
164
+ const i = Number(part);
165
+ if (!Number.isInteger(i) || i < 0) return void 0;
166
+ cur = cur[i];
167
+ } else cur = cur[part];
168
+ if (cur === void 0) return void 0;
169
+ }
170
+ return cur;
171
+ }
172
+ function jsonEqual(a, b) {
173
+ if (a === void 0) return false;
174
+ if (a === b) return true;
175
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
176
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
177
+ if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((x, i) => jsonEqual(x, b[i]));
178
+ const ao = a;
179
+ const bo = b;
180
+ const ak = Object.keys(ao).sort();
181
+ const bk = Object.keys(bo).sort();
182
+ return ak.length === bk.length && ak.every((k, i) => k === bk[i] && jsonEqual(ao[k], bo[k]));
183
+ }
184
+ function matches(record, sel) {
185
+ if (record.target !== sel.target) return false;
186
+ if (sel.op !== void 0 && record.op !== sel.op) return false;
187
+ if (sel.kind !== void 0 && record.kind !== sel.kind) return false;
188
+ if (sel.where) {
189
+ for (const [path, want] of Object.entries(sel.where)) if (!jsonEqual(getPath(record.payload, path), want)) return false;
190
+ }
191
+ if (sel.atLeast) for (const [path, min] of Object.entries(sel.atLeast)) {
192
+ const v = getPath(record.payload, path);
193
+ if (typeof v !== "number" || v < min) return false;
194
+ }
195
+ return true;
196
+ }
197
+ function selectEffects(log, sel) {
198
+ return log.entries.filter((e) => matches(e, sel));
199
+ }
200
+ /** Canonical form of a key, so equal JSON values compare equal whatever their key order. */
201
+ function keyOf(record, path) {
202
+ const v = getPath(record.payload, path);
203
+ return v === void 0 ? void 0 : canonicalJson(v);
204
+ }
205
+ function checkInvariant(log, inv) {
206
+ switch (inv.kind) {
207
+ case "everyRow": return checkEveryRow(log, inv);
208
+ case "implies": return checkImplies(log, inv);
209
+ case "atMostOncePerKey": {
210
+ const seen = /* @__PURE__ */ new Map();
211
+ for (const e of selectEffects(log, inv.effect)) {
212
+ const k = keyOf(e, inv.key);
213
+ if (k === void 0) continue;
214
+ const first = seen.get(k);
215
+ if (first !== void 0) return {
216
+ invariant: inv.name,
217
+ message: `${inv.effect.target} ${inv.effect.op ?? "*"} happened twice for key ${k} (seq ${first} and ${e.seq})`,
218
+ keys: [k],
219
+ evidence: [first, e.seq]
220
+ };
221
+ seen.set(k, e.seq);
222
+ }
223
+ return null;
224
+ }
225
+ case "atLeastOnce": {
226
+ const present = /* @__PURE__ */ new Set();
227
+ const selected = selectEffects(log, inv.effect);
228
+ for (const e of selected) {
229
+ const k = keyOf(e, inv.key);
230
+ if (k !== void 0) present.add(k);
231
+ }
232
+ const missing = inv.expected.map((v) => canonicalJson(v)).filter((k) => !present.has(k));
233
+ if (missing.length === 0) return null;
234
+ return {
235
+ invariant: inv.name,
236
+ message: `${inv.effect.target} ${inv.effect.op ?? "*"} never happened for key(s) ${missing.join(", ")}`,
237
+ keys: missing,
238
+ evidence: selected.map((e) => e.seq)
239
+ };
240
+ }
241
+ }
242
+ }
243
+ function checkEveryRow(log, inv) {
244
+ const rows = queryRows(log, inv.query);
245
+ if (rows === void 0) return {
246
+ invariant: inv.name,
247
+ message: `no snapshot of ${inv.query.target} was recorded`,
248
+ keys: [],
249
+ evidence: []
250
+ };
251
+ const failing = rows.filter((r) => !inv.holds.every((c) => rowMatches(r, c)));
252
+ if (failing.length === 0) return null;
253
+ const keys = failing.map((r) => canonicalJson(r));
254
+ const shown = keys.slice(0, 3).join(", ") + (failing.length > 3 ? ", …" : "");
255
+ return {
256
+ invariant: inv.name,
257
+ message: `${failing.length} row(s) of ${inv.query.table} in ${inv.query.target} violate the condition: ${shown}`,
258
+ keys,
259
+ evidence: snapshotSeq(log, inv.query.target)
260
+ };
261
+ }
262
+ function checkImplies(log, inv) {
263
+ const rows = queryRows(log, inv.state);
264
+ if (rows === void 0) return {
265
+ invariant: inv.name,
266
+ message: `no snapshot of ${inv.state.target} was recorded`,
267
+ keys: [],
268
+ evidence: []
269
+ };
270
+ const present = /* @__PURE__ */ new Set();
271
+ for (const r of rows) {
272
+ const v = r[inv.column];
273
+ if (v !== void 0) present.add(canonicalJson(v));
274
+ }
275
+ const missing = [];
276
+ const evidence = [];
277
+ for (const e of selectEffects(log, inv.when)) {
278
+ const k = keyOf(e, inv.key);
279
+ if (k === void 0 || present.has(k)) continue;
280
+ if (!missing.includes(k)) missing.push(k);
281
+ evidence.push(e.seq);
282
+ }
283
+ if (missing.length === 0) return null;
284
+ return {
285
+ invariant: inv.name,
286
+ message: `${inv.when.target} ${inv.when.op ?? "*"} happened for key(s) ${missing.join(", ")} but ${inv.state.table}.${inv.column} in ${inv.state.target} has no such row`,
287
+ keys: missing,
288
+ evidence
289
+ };
290
+ }
291
+ function checkInvariants(log, invariants) {
292
+ const out = [];
293
+ for (const inv of invariants) {
294
+ const v = checkInvariant(log, inv);
295
+ if (v) out.push(v);
296
+ }
297
+ return out;
298
+ }
299
+ // packages/core/src/scenario.ts
300
+ const defaultBounds = {
301
+ maxFaultsPerRun: 1,
302
+ maxRedeliveries: 2,
303
+ maxConcurrentInvocations: 1,
304
+ budgetMs: 2e3
305
+ };
306
+ function isPositiveInt(n) {
307
+ return typeof n === "number" && Number.isInteger(n) && n > 0;
308
+ }
309
+ /** Every reason a spec is not usable; empty when it is. */
310
+ function validateScenario(spec) {
311
+ const errors = [];
312
+ if (spec.version !== 1) errors.push(`version must be 1, got ${String(spec.version)}`);
313
+ if (!spec.name.trim()) errors.push("name must not be empty");
314
+ if (spec.platform !== void 0 && !spec.platform) errors.push("platform must not be empty when given");
315
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(spec.name)) errors.push(`name must be lowercase letters, digits and dashes: ${JSON.stringify(spec.name)}`);
316
+ if (!spec.worker.entry) errors.push("worker.entry must not be empty");
317
+ if (!spec.worker.config) errors.push("worker.config must not be empty");
318
+ if (!spec.drive.kind) errors.push("drive.kind must not be empty");
319
+ spec.setup.forEach((s, i) => {
320
+ if (!s.kind) errors.push(`setup[${i}].kind must not be empty`);
321
+ });
322
+ if (spec.invariants.length === 0) errors.push("at least one invariant is required");
323
+ const names = /* @__PURE__ */ new Set();
324
+ for (const inv of spec.invariants) {
325
+ if (!inv.name) errors.push("every invariant needs a name");
326
+ else if (names.has(inv.name)) errors.push(`duplicate invariant name ${JSON.stringify(inv.name)}`);
327
+ names.add(inv.name);
328
+ }
329
+ const ext = /* @__PURE__ */ new Set();
330
+ for (const e of spec.externals) {
331
+ if (!e.name) errors.push("every external needs a name");
332
+ if (ext.has(e.name)) errors.push(`duplicate external ${JSON.stringify(e.name)}`);
333
+ ext.add(e.name);
334
+ if (!e.host) errors.push(`external ${JSON.stringify(e.name)} needs a host`);
335
+ (e.responses ?? []).forEach((r, i) => {
336
+ if (!(Number.isInteger(r.status) && r.status >= 100 && r.status <= 599)) errors.push(`external ${JSON.stringify(e.name)} responses[${i}].status must be an HTTP status`);
337
+ });
338
+ }
339
+ for (const k of [
340
+ "maxFaultsPerRun",
341
+ "maxRedeliveries",
342
+ "maxConcurrentInvocations",
343
+ "budgetMs"
344
+ ]) if (k === "maxFaultsPerRun" || k === "maxRedeliveries") {
345
+ const v = spec.bounds[k];
346
+ if (!(Number.isInteger(v) && v >= 0)) errors.push(`bounds.${k} must be a non-negative integer`);
347
+ } else if (!isPositiveInt(spec.bounds[k])) errors.push(`bounds.${k} must be a positive integer`);
348
+ return errors;
349
+ }
350
+ // packages/core/src/shrink.ts
351
+ /** One-step candidates: each single fault removed, then each single event removed. */
352
+ function shrinkCandidates(state) {
353
+ const out = [];
354
+ state.schedule.faults.forEach((_, i) => {
355
+ out.push({
356
+ schedule: {
357
+ seed: state.schedule.seed,
358
+ faults: state.schedule.faults.filter((_, j) => j !== i)
359
+ },
360
+ keepIds: state.keepIds
361
+ });
362
+ });
363
+ if (state.keepIds.length > 1) state.keepIds.forEach((id) => {
364
+ out.push({
365
+ schedule: {
366
+ seed: state.schedule.seed,
367
+ faults: state.schedule.faults.filter((f) => !f.invocation.startsWith(`${id}#`))
368
+ },
369
+ keepIds: state.keepIds.filter((k) => k !== id)
370
+ });
371
+ });
372
+ return out;
373
+ }
374
+ // packages/engine/src/clock.ts
375
+ /**
376
+ * The virtual clock and the globals patch. Within a run, Date, Math.random,
377
+ * crypto.randomUUID, crypto.getRandomValues, performance.now and the timer
378
+ * functions are replaced by deterministic versions driven by the run's seed
379
+ * and by a clock that starts at a fixed epoch and advances only when the
380
+ * engine says so: one millisecond per effect, and to each timer's due time
381
+ * when the timer fires. Timers fire only while the invocation is idle (no
382
+ * effect in flight), in due order, so they never race real I/O.
383
+ */
384
+ /** 2026-01-01T00:00:00.000Z. Every run starts here. */
385
+ const virtualEpoch = Date.UTC(2026, 0, 1);
386
+ const realSetTimeout = globalThis.setTimeout.bind(globalThis);
387
+ const realSetInterval = globalThis.setInterval.bind(globalThis);
388
+ const realClearTimeout = globalThis.clearTimeout.bind(globalThis);
389
+ const realClearInterval = globalThis.clearInterval.bind(globalThis);
390
+ const libraryScope = (() => {
391
+ const mod = typeof process !== "undefined" ? process?.getBuiltinModule?.("node:async_hooks") : void 0;
392
+ return mod?.AsyncLocalStorage ? new mod.AsyncLocalStorage() : null;
393
+ })();
394
+ var VirtualClock = class {
395
+ time = virtualEpoch;
396
+ timers = [];
397
+ nextId = 1;
398
+ /** The invocation whose code is running; timers scheduled now belong to it. */
399
+ owner = null;
400
+ now() {
401
+ return this.time;
402
+ }
403
+ advance(ms) {
404
+ if (ms > 0) this.time += ms;
405
+ }
406
+ schedule(callback, delay, args, interval) {
407
+ const ms = Number.isFinite(delay) && delay > 0 ? Math.floor(delay) : 0;
408
+ const timer = {
409
+ id: this.nextId++,
410
+ due: this.time + ms,
411
+ callback,
412
+ args,
413
+ interval,
414
+ owner: this.owner
415
+ };
416
+ this.timers.push(timer);
417
+ return timer.id;
418
+ }
419
+ setTimeout(callback, delay = 0, ...args) {
420
+ return this.schedule(callback, delay, args, null);
421
+ }
422
+ setInterval(callback, delay = 0, ...args) {
423
+ return this.schedule(callback, delay, args, Math.max(1, Math.floor(delay)));
424
+ }
425
+ clear(id) {
426
+ this.timers = this.timers.filter((t) => t.id !== id);
427
+ }
428
+ /** Whether this clock issued the handle (and the timer is still pending). */
429
+ owns(id) {
430
+ return typeof id === "number" && this.timers.some((t) => t.id === id);
431
+ }
432
+ pendingTimers() {
433
+ return this.timers.length;
434
+ }
435
+ /** Drop every timer an invocation scheduled: a crashed invocation's code must never run again. */
436
+ cancelOwner(owner) {
437
+ this.timers = this.timers.filter((t) => t.owner !== owner);
438
+ }
439
+ /** Advance to the earliest due timer and run it. False when none is pending. */
440
+ fireNext() {
441
+ if (this.timers.length === 0) return false;
442
+ let earliest = this.timers[0];
443
+ for (const t of this.timers) if (t.due < earliest.due || t.due === earliest.due && t.id < earliest.id) earliest = t;
444
+ if (earliest.due > this.time) this.time = earliest.due;
445
+ if (earliest.interval === null) this.timers = this.timers.filter((t) => t !== earliest);
446
+ else earliest.due = this.time + earliest.interval;
447
+ try {
448
+ earliest.callback(...earliest.args);
449
+ } catch {}
450
+ return true;
451
+ }
452
+ };
453
+ /** Deterministic randomness for a run: Math.random, UUIDs and random bytes from one seeded stream. */
454
+ var VirtualRandom = class {
455
+ state;
456
+ constructor(seed) {
457
+ this.state = seedPrng(`random:${seed}`);
458
+ }
459
+ float() {
460
+ const [f, next] = nextFloat(this.state);
461
+ this.state = next;
462
+ return f;
463
+ }
464
+ u32() {
465
+ const [u, next] = nextU32(this.state);
466
+ this.state = next;
467
+ return u;
468
+ }
469
+ fill(view) {
470
+ const bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
471
+ for (let i = 0; i < bytes.length; i += 4) {
472
+ const u = this.u32();
473
+ for (let j = 0; j < 4 && i + j < bytes.length; j++) bytes[i + j] = u >>> 8 * j & 255;
474
+ }
475
+ return view;
476
+ }
477
+ /** A version-4 UUID from the stream. */
478
+ uuid() {
479
+ const b = this.fill(/* @__PURE__ */ new Uint8Array(16));
480
+ b[6] = b[6] & 15 | 64;
481
+ b[8] = b[8] & 63 | 128;
482
+ const hex = [...b].map((x) => x.toString(16).padStart(2, "0")).join("");
483
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
484
+ }
485
+ };
486
+ /** Replace the nondeterministic globals for one run. Returns the handle that restores them. */
487
+ function patchGlobals(seed) {
488
+ const clock = new VirtualClock();
489
+ const random = new VirtualRandom(seed);
490
+ const g = globalThis;
491
+ const saved = /* @__PURE__ */ new Map();
492
+ const define = (target, key, value) => {
493
+ const owner = target === g ? g : target;
494
+ saved.set(key, Object.getOwnPropertyDescriptor(owner, key));
495
+ Object.defineProperty(owner, key, {
496
+ value,
497
+ configurable: true,
498
+ writable: true,
499
+ enumerable: false
500
+ });
501
+ };
502
+ const RealDate = Date;
503
+ const VirtualDate = function(...args) {
504
+ if (new.target === void 0) return new RealDate(clock.now()).toString();
505
+ return Reflect.construct(RealDate, args.length === 0 ? [clock.now()] : args, new.target);
506
+ };
507
+ Object.setPrototypeOf(VirtualDate, RealDate);
508
+ Object.defineProperty(VirtualDate, "prototype", {
509
+ value: RealDate.prototype,
510
+ writable: false
511
+ });
512
+ Object.defineProperty(VirtualDate, "now", {
513
+ value: () => clock.now(),
514
+ configurable: true,
515
+ writable: true
516
+ });
517
+ Object.defineProperty(VirtualDate, "name", { value: "Date" });
518
+ define(g, "Date", VirtualDate);
519
+ define(Math, "random", () => random.float());
520
+ const inLibrary = () => libraryScope?.getStore() === true;
521
+ define(g, "setTimeout", (cb, ms, ...args) => inLibrary() ? realSetTimeout(cb, ms, ...args) : clock.setTimeout(cb, ms, ...args));
522
+ define(g, "setInterval", (cb, ms, ...args) => inLibrary() ? realSetInterval(cb, ms, ...args) : clock.setInterval(cb, ms, ...args));
523
+ define(g, "clearTimeout", (id) => {
524
+ if (clock.owns(id)) clock.clear(id);
525
+ else realClearTimeout(id);
526
+ });
527
+ define(g, "clearInterval", (id) => {
528
+ if (clock.owns(id)) clock.clear(id);
529
+ else realClearInterval(id);
530
+ });
531
+ const perf = g["performance"];
532
+ if (perf && typeof perf === "object") define(perf, "now", () => clock.now() - virtualEpoch);
533
+ const cryptoObj = g["crypto"];
534
+ if (cryptoObj && typeof cryptoObj === "object") {
535
+ define(cryptoObj, "randomUUID", () => random.uuid());
536
+ define(cryptoObj, "getRandomValues", (view) => random.fill(view));
537
+ }
538
+ let restored = false;
539
+ const targets = {
540
+ Date: g,
541
+ random: Math,
542
+ setTimeout: g,
543
+ setInterval: g,
544
+ clearTimeout: g,
545
+ clearInterval: g
546
+ };
547
+ if (perf && typeof perf === "object") targets["now"] = perf;
548
+ if (cryptoObj && typeof cryptoObj === "object") {
549
+ targets["randomUUID"] = cryptoObj;
550
+ targets["getRandomValues"] = cryptoObj;
551
+ }
552
+ return {
553
+ clock,
554
+ random,
555
+ libraryIo(perform) {
556
+ return libraryScope ? libraryScope.run(true, perform) : perform();
557
+ },
558
+ restore() {
559
+ if (restored) return;
560
+ restored = true;
561
+ for (const [key, descriptor] of saved) {
562
+ const target = targets[key];
563
+ if (descriptor) Object.defineProperty(target, key, descriptor);
564
+ else delete target[key];
565
+ }
566
+ }
567
+ };
568
+ }
569
+ /** Yield to the real event loop so microtasks and pending I/O can make progress. */
570
+ function realTick() {
571
+ return new Promise((resolve) => realSetTimeout(() => resolve(), 0));
572
+ }
573
+ /**
574
+ * Wait for a promise under the virtual clock: while it is pending, let real
575
+ * I/O complete, and when the invocation is idle fire the next due timer.
576
+ */
577
+ async function settleOn(promise, ctx) {
578
+ let done = false;
579
+ let failed = false;
580
+ let value;
581
+ let error;
582
+ const tracked = promise.then((v) => {
583
+ done = true;
584
+ value = v;
585
+ }, (e) => {
586
+ done = true;
587
+ failed = true;
588
+ error = e;
589
+ });
590
+ for (;;) {
591
+ await Promise.race([tracked, realTick()]);
592
+ if (done) {
593
+ if (failed) throw error;
594
+ return value;
595
+ }
596
+ if (ctx.crashed()) return new Promise(() => {});
597
+ if (ctx.inFlight() > 0) continue;
598
+ if (ctx.clock.fireNext()) continue;
599
+ ctx.stuck();
600
+ return new Promise(() => {});
601
+ }
602
+ }
603
+ // packages/engine/src/fetch.ts
604
+ const FAULTS = [
605
+ "crash_before",
606
+ "crash_after",
607
+ "ambiguous",
608
+ "error"
609
+ ];
610
+ /** Match a host against an exact host or a "*.suffix" pattern. */
611
+ function hostMatches(pattern, host) {
612
+ if (pattern.startsWith("*.")) {
613
+ const suffix = pattern.slice(1);
614
+ return host.endsWith(suffix) && host.length > suffix.length;
615
+ }
616
+ return pattern === host;
617
+ }
618
+ function findExternal(externals, host) {
619
+ return externals.find((e) => hostMatches(e.host, host));
620
+ }
621
+ function findResponse(external, method, path) {
622
+ if (!external.responses) return null;
623
+ return external.responses.find((r) => {
624
+ if (r.method && r.method.toUpperCase() !== method) return false;
625
+ if (r.path === void 0) return true;
626
+ return r.path.endsWith("*") ? path.startsWith(r.path.slice(0, -1)) : r.path === path;
627
+ });
628
+ }
629
+ const REDIRECTS = /* @__PURE__ */ new Set([
630
+ 301,
631
+ 302,
632
+ 303,
633
+ 307,
634
+ 308
635
+ ]);
636
+ function replyOf(r) {
637
+ if (r === null) return {
638
+ status: 200,
639
+ headers: {},
640
+ body: ""
641
+ };
642
+ const json = r.body !== void 0 && typeof r.body !== "string";
643
+ const headers = {};
644
+ for (const [name, value] of Object.entries(r.headers ?? {})) headers[name.toLowerCase()] = value;
645
+ if (json && !("content-type" in headers)) headers["content-type"] = "application/json";
646
+ const body = r.body === void 0 ? "" : json ? JSON.stringify(r.body) : r.body;
647
+ return {
648
+ status: r.status,
649
+ headers,
650
+ body
651
+ };
652
+ }
653
+ function toResponse(reply) {
654
+ return new Response(reply.body === "" ? null : reply.body, {
655
+ status: reply.status,
656
+ headers: reply.headers
657
+ });
658
+ }
659
+ function parseBody(text) {
660
+ if (text === "") return null;
661
+ try {
662
+ return JSON.parse(text);
663
+ } catch {
664
+ return text;
665
+ }
666
+ }
667
+ /**
668
+ * Build the replacement `fetch`. With `hooks`, every request is an effect
669
+ * point recorded as `Fetch:<external>` with the method as op; the result
670
+ * summary carries `status`, `effective` and the dedupe key.
671
+ */
672
+ function createScriptedFetch(externals, hooks, unsupported, afterRequest = () => {}) {
673
+ const dedupe = /* @__PURE__ */ new Map();
674
+ const scripted = (async (input, init) => {
675
+ const request = new Request(input, init);
676
+ const url = new URL(request.url);
677
+ const method = request.method.toUpperCase();
678
+ const external = findExternal(externals, url.host);
679
+ if (!external) {
680
+ unsupported(`fetch to undeclared host ${url.host}; declare it as an external in the scenario`);
681
+ throw new TypeError(`fetch failed: ${url.host} is not a declared external`);
682
+ }
683
+ const headers = {};
684
+ request.headers.forEach((value, name) => {
685
+ headers[name.toLowerCase()] = value;
686
+ });
687
+ const dedupeHeader = external.guarantees.dedupeBy?.toLowerCase();
688
+ const dedupeKey = dedupeHeader ? headers[dedupeHeader] ?? null : null;
689
+ const body = parseBody(await request.text());
690
+ const args = {
691
+ url: request.url,
692
+ method,
693
+ headers,
694
+ body,
695
+ dedupeKey
696
+ };
697
+ const perform = async () => {
698
+ const memo = dedupeKey === null ? null : `${external.name}\u0000${dedupeKey}`;
699
+ if (memo !== null) {
700
+ const earlier = dedupe.get(memo);
701
+ if (earlier) return {
702
+ reply: earlier,
703
+ effective: false
704
+ };
705
+ }
706
+ const script = findResponse(external, method, url.pathname);
707
+ if (script === void 0) {
708
+ unsupported(`no scripted response in external ${external.name} for ${method} ${url.pathname}`);
709
+ throw new TypeError(`fetch failed: no scripted response for ${method} ${url.pathname}`);
710
+ }
711
+ const reply = replyOf(script);
712
+ if (REDIRECTS.has(reply.status) && request.redirect !== "manual") {
713
+ unsupported(`external ${external.name} answers ${method} ${url.pathname} with a ${reply.status} redirect; script the final reply or fetch with redirect: "manual"`);
714
+ throw new TypeError(`fetch failed: redirects are not modelled (${reply.status} from ${url.host})`);
715
+ }
716
+ if (memo !== null) dedupe.set(memo, reply);
717
+ return {
718
+ reply,
719
+ effective: true
720
+ };
721
+ };
722
+ if (!hooks) {
723
+ const { reply } = await perform();
724
+ afterRequest();
725
+ return toResponse(reply);
726
+ }
727
+ const { reply } = await hooks.hooks.effect({
728
+ invocation: hooks.invocation(),
729
+ target: `Fetch:${external.name}`,
730
+ op: method,
731
+ args,
732
+ allowed: FAULTS
733
+ }, perform, (r) => ({
734
+ status: r.reply.status,
735
+ effective: r.effective,
736
+ dedupeKey
737
+ }));
738
+ return toResponse(reply);
739
+ });
740
+ return {
741
+ fetch: scripted,
742
+ dedupe
743
+ };
744
+ }
745
+ /** Replace the global `fetch` for a run; returns the function that restores it. */
746
+ function installScriptedFetch(externals, hooks, unsupported, afterRequest) {
747
+ const g = globalThis;
748
+ const saved = Object.getOwnPropertyDescriptor(globalThis, "fetch");
749
+ const { fetch: scripted } = createScriptedFetch(externals, hooks, unsupported, afterRequest);
750
+ Object.defineProperty(globalThis, "fetch", {
751
+ value: scripted,
752
+ configurable: true,
753
+ writable: true
754
+ });
755
+ let restored = false;
756
+ return () => {
757
+ if (restored) return;
758
+ restored = true;
759
+ if (saved) Object.defineProperty(globalThis, "fetch", saved);
760
+ else delete g.fetch;
761
+ };
762
+ }
763
+ // packages/engine/src/run.ts
764
+ /**
765
+ * One run of a scenario: the effect log being built, the fault decisions
766
+ * (drawn during exploration, looked up during replay), and the invocations
767
+ * in flight. Proxies in the platform package call `effect`/`effectSync`
768
+ * around every real effect; everything else is engine-internal.
769
+ */
770
+ /** Thrown into user code by a synchronous effect that crashes the invocation. */
771
+ var CrashSignal = class extends Error {
772
+ name = "CrashSignal";
773
+ invocation;
774
+ constructor(invocation) {
775
+ super(`invocation ${invocation} crashed by an injected fault`);
776
+ this.invocation = invocation;
777
+ }
778
+ };
779
+ /** The error user code sees for an injected `error` or `ambiguous` fault. */
780
+ var InjectedEffectError = class extends Error {
781
+ name = "InjectedEffectError";
782
+ point;
783
+ fault;
784
+ constructor(point, fault) {
785
+ super(`${point.target} ${point.op} failed (injected ${fault})`);
786
+ this.point = point;
787
+ this.fault = fault;
788
+ }
789
+ };
790
+ var InvocationState = class {
791
+ crashed;
792
+ onCrash;
793
+ /** Effect points seen so far in this invocation; the next effect's ordinal. */
794
+ ordinal = 0;
795
+ crashedFlag = false;
796
+ resolveCrashed = () => {};
797
+ id;
798
+ constructor(id, onCrash) {
799
+ this.id = id;
800
+ this.onCrash = onCrash;
801
+ this.crashed = new Promise((resolve) => {
802
+ this.resolveCrashed = resolve;
803
+ });
804
+ }
805
+ isCrashed() {
806
+ return this.crashedFlag;
807
+ }
808
+ crash() {
809
+ if (this.crashedFlag) return;
810
+ this.crashedFlag = true;
811
+ this.onCrash();
812
+ this.resolveCrashed();
813
+ }
814
+ };
815
+ /** A promise that never settles: how an abandoned invocation experiences a crash. */
816
+ function hang() {
817
+ return new Promise(() => {});
818
+ }
819
+ var Run = class {
820
+ logState = emptyLog;
821
+ faults = [];
822
+ effectIndex = 0;
823
+ prng;
824
+ invocations = /* @__PURE__ */ new Map();
825
+ reasons = [];
826
+ inFlight = 0;
827
+ patched = null;
828
+ restoreFetch = null;
829
+ /** The invocation whose code is running (one at a time); outbound fetches are attributed to it. */
830
+ current = "";
831
+ options;
832
+ constructor(options) {
833
+ this.options = options;
834
+ this.prng = seedPrng(options.schedule.seed);
835
+ }
836
+ get log() {
837
+ return this.logState;
838
+ }
839
+ /** The schedule as actually executed: input decisions during replay, drawn ones during exploration. */
840
+ get schedule() {
841
+ return {
842
+ seed: this.options.schedule.seed,
843
+ faults: [...this.faults]
844
+ };
845
+ }
846
+ get effectPoints() {
847
+ return this.effectIndex;
848
+ }
849
+ get faultsInjected() {
850
+ return this.faults.length;
851
+ }
852
+ get unsupportedReasons() {
853
+ return this.reasons;
854
+ }
855
+ /** Replace the nondeterministic globals and `fetch` for this run. Call `uninstall` in a finally. */
856
+ install(externals = []) {
857
+ if (!this.patched) this.patched = patchGlobals(this.options.schedule.seed);
858
+ if (!this.restoreFetch) this.restoreFetch = installScriptedFetch(externals, {
859
+ hooks: this,
860
+ invocation: () => this.current
861
+ }, (reason) => this.unsupported(reason));
862
+ }
863
+ uninstall() {
864
+ this.restoreFetch?.();
865
+ this.restoreFetch = null;
866
+ this.patched?.restore();
867
+ this.patched = null;
868
+ }
869
+ /** Virtual milliseconds since the epoch, or the real clock when the globals are not installed. */
870
+ now() {
871
+ return this.patched ? this.patched.clock.now() : Date.now();
872
+ }
873
+ /** Timers scheduled and not yet fired or cancelled (0 when the globals are not installed). */
874
+ pendingTimers() {
875
+ return this.patched?.clock.pendingTimers() ?? 0;
876
+ }
877
+ startInvocation(id) {
878
+ if (this.invocations.has(id)) throw new Error(`invocation ${id} already started`);
879
+ const inv = new InvocationState(id, () => this.patched?.clock.cancelOwner(id));
880
+ this.invocations.set(id, inv);
881
+ if (this.patched) this.patched.clock.owner = id;
882
+ this.current = id;
883
+ return inv;
884
+ }
885
+ invocation(id) {
886
+ const inv = this.invocations.get(id);
887
+ if (!inv) throw new Error(`effect from unknown invocation ${id}`);
888
+ return inv;
889
+ }
890
+ decide(point, ordinal) {
891
+ if (this.options.mode === "replay") {
892
+ const d = this.options.schedule.faults.find((f) => f.invocation === point.invocation && f.ordinal === ordinal);
893
+ return d && point.allowed.includes(d.fault) ? d.fault : null;
894
+ }
895
+ const [fault, next] = decideFault(this.prng, point.allowed, this.faults.length, {
896
+ maxFaultsPerRun: this.options.bounds.maxFaultsPerRun,
897
+ faultProbability: this.options.faultProbability
898
+ });
899
+ this.prng = next;
900
+ return fault;
901
+ }
902
+ record(point, kind, payload) {
903
+ this.logState = appendEffect(this.logState, {
904
+ kind,
905
+ invocation: point.invocation,
906
+ target: point.target,
907
+ op: point.op,
908
+ payload
909
+ });
910
+ }
911
+ /** Open an effect point: record the call and decide the fault. */
912
+ open(point, inv) {
913
+ this.effectIndex++;
914
+ const ordinal = inv.ordinal++;
915
+ this.record(point, "call", { args: point.args });
916
+ const fault = this.decide(point, ordinal);
917
+ if (fault) this.faults.push({
918
+ invocation: point.invocation,
919
+ ordinal,
920
+ fault,
921
+ target: point.target,
922
+ op: point.op
923
+ });
924
+ return fault;
925
+ }
926
+ async effect(point, perform, summarize) {
927
+ const inv = this.invocation(point.invocation);
928
+ if (inv.isCrashed()) return hang();
929
+ const fault = this.open(point, inv);
930
+ if (fault === "crash_before" || fault === "cut_off") {
931
+ this.record(point, "fault", {
932
+ args: point.args,
933
+ fault
934
+ });
935
+ inv.crash();
936
+ return hang();
937
+ }
938
+ if (fault === "error") {
939
+ this.record(point, "fault", {
940
+ args: point.args,
941
+ fault
942
+ });
943
+ throw new InjectedEffectError(point, fault);
944
+ }
945
+ let value;
946
+ this.inFlight++;
947
+ try {
948
+ value = await (this.patched ? this.patched.libraryIo(perform) : perform());
949
+ } catch (err) {
950
+ this.record(point, "result", {
951
+ args: point.args,
952
+ result: null,
953
+ error: String(err)
954
+ });
955
+ throw err;
956
+ } finally {
957
+ this.inFlight--;
958
+ this.patched?.clock.advance(1);
959
+ }
960
+ this.record(point, "result", {
961
+ args: point.args,
962
+ result: summarize(value)
963
+ });
964
+ if (inv.isCrashed()) return hang();
965
+ if (fault === "crash_after") {
966
+ this.record(point, "fault", {
967
+ args: point.args,
968
+ fault
969
+ });
970
+ inv.crash();
971
+ return hang();
972
+ }
973
+ if (fault === "ambiguous") {
974
+ this.record(point, "fault", {
975
+ args: point.args,
976
+ fault
977
+ });
978
+ throw new InjectedEffectError(point, fault);
979
+ }
980
+ return value;
981
+ }
982
+ effectSync(point, perform, summarize) {
983
+ const inv = this.invocation(point.invocation);
984
+ if (inv.isCrashed()) throw new CrashSignal(inv.id);
985
+ const fault = this.open(point, inv);
986
+ if (fault === "crash_before" || fault === "cut_off") {
987
+ this.record(point, "fault", {
988
+ args: point.args,
989
+ fault
990
+ });
991
+ inv.crash();
992
+ throw new CrashSignal(inv.id);
993
+ }
994
+ if (fault === "error") {
995
+ this.record(point, "fault", {
996
+ args: point.args,
997
+ fault
998
+ });
999
+ throw new InjectedEffectError(point, fault);
1000
+ }
1001
+ let value;
1002
+ try {
1003
+ value = perform();
1004
+ } catch (err) {
1005
+ this.record(point, "result", {
1006
+ args: point.args,
1007
+ result: null,
1008
+ error: String(err)
1009
+ });
1010
+ throw err;
1011
+ }
1012
+ this.record(point, "result", {
1013
+ args: point.args,
1014
+ result: summarize(value)
1015
+ });
1016
+ if (fault === "crash_after") {
1017
+ this.record(point, "fault", {
1018
+ args: point.args,
1019
+ fault
1020
+ });
1021
+ inv.crash();
1022
+ throw new CrashSignal(inv.id);
1023
+ }
1024
+ if (fault === "ambiguous") {
1025
+ this.record(point, "fault", {
1026
+ args: point.args,
1027
+ fault
1028
+ });
1029
+ throw new InjectedEffectError(point, fault);
1030
+ }
1031
+ return value;
1032
+ }
1033
+ unsupported(reason) {
1034
+ this.reasons.push(reason);
1035
+ }
1036
+ settle(promise, invocation) {
1037
+ const inv = this.invocation(invocation);
1038
+ const patched = this.patched;
1039
+ if (patched) patched.clock.owner = invocation;
1040
+ this.current = invocation;
1041
+ if (!patched) return Promise.race([promise, inv.crashed.then(() => new Promise(() => {}))]);
1042
+ return settleOn(promise, {
1043
+ clock: patched.clock,
1044
+ inFlight: () => this.inFlight,
1045
+ crashed: () => inv.isCrashed(),
1046
+ stuck: () => {
1047
+ this.unsupported(`invocation ${invocation} is waiting on something Eutaxy cannot see (no effect in flight, no timer due)`);
1048
+ inv.crash();
1049
+ }
1050
+ });
1051
+ }
1052
+ snapshot(invocation, target, payload) {
1053
+ this.logState = appendEffect(this.logState, {
1054
+ kind: "snapshot",
1055
+ invocation,
1056
+ target,
1057
+ op: "snapshot",
1058
+ payload
1059
+ });
1060
+ }
1061
+ };
1062
+ // packages/engine/src/shrink.ts
1063
+ /**
1064
+ * Shrinking, the loop: try the pure candidates in order, replay each, keep
1065
+ * the first that still violates the same invariant for the same keys, and
1066
+ * repeat from there until nothing smaller works or the budget is spent.
1067
+ */
1068
+ /** The scenario restricted to the kept events, with per-message invariants re-derived. */
1069
+ function restrictSpec(platform, spec, keepIds) {
1070
+ const restricted = platform.restrictEvents(spec, keepIds);
1071
+ const bodies = platform.initialEvents(restricted).map((e) => e.data);
1072
+ return {
1073
+ ...restricted,
1074
+ invariants: recomputePerMessageExpected(restricted.invariants, bodies)
1075
+ };
1076
+ }
1077
+ async function shrink(spec, platform, schedule, violation, trace, options) {
1078
+ const allIds = platform.initialEvents(spec).map((e) => e.id);
1079
+ let state = {
1080
+ schedule,
1081
+ keepIds: allIds
1082
+ };
1083
+ let best = {
1084
+ trace,
1085
+ violation
1086
+ };
1087
+ let runs = 0;
1088
+ let improved = true;
1089
+ while (improved && runs < options.maxRuns && options.now() < options.deadline) {
1090
+ improved = false;
1091
+ for (const candidate of shrinkCandidates(state)) {
1092
+ if (runs >= options.maxRuns || options.now() >= options.deadline) break;
1093
+ const candidateSpec = restrictSpec(platform, spec, candidate.keepIds);
1094
+ const run = await replayRun(candidateSpec, platform, candidate.schedule);
1095
+ const log = run.log;
1096
+ runs++;
1097
+ if (run.unsupportedReasons.length > 0) continue;
1098
+ const found = checkInvariants(log, candidateSpec.invariants).find((v) => sameViolation(v, violation));
1099
+ if (found) {
1100
+ state = candidate;
1101
+ best = {
1102
+ trace: log,
1103
+ violation: found
1104
+ };
1105
+ improved = true;
1106
+ break;
1107
+ }
1108
+ }
1109
+ }
1110
+ return {
1111
+ spec: restrictSpec(platform, spec, state.keepIds),
1112
+ schedule: state.schedule,
1113
+ keptEvents: state.keepIds,
1114
+ trace: best.trace,
1115
+ violation: best.violation,
1116
+ runs,
1117
+ faultsRemoved: schedule.faults.length - state.schedule.faults.length,
1118
+ eventsRemoved: allIds.length - state.keepIds.length
1119
+ };
1120
+ }
1121
+ // packages/engine/src/explore.ts
1122
+ /**
1123
+ * The exploration loop: run the scenario under seeded fault injection until
1124
+ * an invariant breaks or the budget is spent; replay any violation before
1125
+ * reporting it.
1126
+ */
1127
+ const engineAssumptions = [
1128
+ "schedule points are effect boundaries: proxied bindings, patched timers and fetch; other awaits run in program order",
1129
+ "faults: crash_before, crash_after, ambiguous, error at any effect point; redelivery per the platform's queue model",
1130
+ "one invocation at a time (maxConcurrentInvocations = 1)",
1131
+ "Date, Math.random, crypto.randomUUID, crypto.getRandomValues, performance.now and timers are virtualised per run from the seed; time starts at 2026-01-01T00:00:00Z and advances 1 ms per effect and to each timer's due time",
1132
+ "timers fire only while the invocation is idle (no effect in flight), in due order; an invocation waiting on something outside Eutaxy makes the run UNSUPPORTED",
1133
+ "while an effect performs real I/O the real timer functions are in place for that I/O, so user code that schedules a timer concurrently with an effect gets a real one",
1134
+ "module-level state cannot be reset between runs; before BOUNDED the first runs are replayed in reverse order and any difference makes the run UNSUPPORTED (module_state_leak)",
1135
+ "outbound fetch never reaches the network: requests are answered from the scenario's declared externals, an external's dedupeBy guarantee is honoured by replaying the first response per key, and an undeclared host makes the run UNSUPPORTED"
1136
+ ];
1137
+ /** Run the scenario once under the given run's fault decisions and return its log. */
1138
+ async function runOnce(spec, platform, run) {
1139
+ await platform.reset(spec);
1140
+ run.install(spec.externals);
1141
+ try {
1142
+ try {
1143
+ const pending = [...platform.initialEvents(spec)];
1144
+ while (pending.length > 0) {
1145
+ const event = pending.shift();
1146
+ const result = await platform.deliver(event, run, spec);
1147
+ pending.push(...result.followUps);
1148
+ }
1149
+ } finally {
1150
+ run.uninstall();
1151
+ }
1152
+ await platform.finish?.(spec, run);
1153
+ } finally {
1154
+ await platform.cleanup?.(spec);
1155
+ }
1156
+ return run.log;
1157
+ }
1158
+ /** Re-run a recorded schedule exactly and return the whole run (log and support status). */
1159
+ async function replayRun(spec, platform, schedule) {
1160
+ const run = new Run({
1161
+ mode: "replay",
1162
+ schedule,
1163
+ bounds: spec.bounds,
1164
+ faultProbability: 0
1165
+ });
1166
+ await runOnce(spec, platform, run);
1167
+ return run;
1168
+ }
1169
+ /** Re-run a recorded schedule exactly. */
1170
+ async function replay(spec, platform, schedule) {
1171
+ return (await replayRun(spec, platform, schedule)).log;
1172
+ }
1173
+ async function explore(spec, platform, options) {
1174
+ const now = options.now ?? (() => Date.now());
1175
+ const maxRuns = options.maxRuns ?? 100;
1176
+ const faultProbability = options.faultProbability ?? .5;
1177
+ const assumptions = [...engineAssumptions, ...platform.assumptions];
1178
+ const start = now();
1179
+ const reached = {
1180
+ runs: 0,
1181
+ effectPoints: 0,
1182
+ faultsInjected: 0
1183
+ };
1184
+ const done = () => ({
1185
+ ...reached,
1186
+ elapsedMs: now() - start
1187
+ });
1188
+ const base = {
1189
+ scenario: spec.name,
1190
+ seed: options.seed,
1191
+ assumptions
1192
+ };
1193
+ if (spec.bounds.maxConcurrentInvocations !== 1) return {
1194
+ ...base,
1195
+ kind: "UNSUPPORTED",
1196
+ cause: "unsupported_bounds",
1197
+ reason: "maxConcurrentInvocations above 1 is not supported yet",
1198
+ reached: done()
1199
+ };
1200
+ const leakCheckRuns = options.leakCheckRuns ?? 2;
1201
+ /** The first runs' schedules and digests, replayed in reverse order before BOUNDED. */
1202
+ const witnesses = [];
1203
+ let master = seedPrng(options.seed);
1204
+ while (reached.runs < maxRuns && now() - start < spec.bounds.budgetMs) {
1205
+ const [runSeed, next] = nextU32(master);
1206
+ master = next;
1207
+ const run = new Run({
1208
+ mode: "explore",
1209
+ schedule: {
1210
+ seed: runSeed,
1211
+ faults: []
1212
+ },
1213
+ bounds: spec.bounds,
1214
+ faultProbability
1215
+ });
1216
+ const log = await runOnce(spec, platform, run);
1217
+ reached.runs++;
1218
+ reached.effectPoints += run.effectPoints;
1219
+ reached.faultsInjected += run.faultsInjected;
1220
+ if (witnesses.length < leakCheckRuns) witnesses.push({
1221
+ schedule: run.schedule,
1222
+ digest: digestLog(log)
1223
+ });
1224
+ if (run.unsupportedReasons.length > 0) return {
1225
+ ...base,
1226
+ kind: "UNSUPPORTED",
1227
+ cause: "unsupported_api",
1228
+ reason: run.unsupportedReasons.join("; "),
1229
+ reached: done()
1230
+ };
1231
+ const violation = checkInvariants(log, spec.invariants)[0];
1232
+ if (violation) {
1233
+ const schedule = run.schedule;
1234
+ const replayedRun = await replayRun(spec, platform, schedule);
1235
+ const replayed = replayedRun.log;
1236
+ reached.runs++;
1237
+ if (replayedRun.unsupportedReasons.length > 0 || digestLog(replayed) !== digestLog(log)) return {
1238
+ ...base,
1239
+ kind: "UNSUPPORTED",
1240
+ cause: "not_replayable",
1241
+ reason: `the violating run of seed ${schedule.seed} did not reproduce on replay`,
1242
+ reached: done()
1243
+ };
1244
+ const shrunk = await shrink(spec, platform, schedule, violation, replayed, {
1245
+ maxRuns: options.maxShrinkRuns ?? 200,
1246
+ deadline: now() + spec.bounds.budgetMs,
1247
+ now
1248
+ });
1249
+ reached.runs += shrunk.runs;
1250
+ return {
1251
+ ...base,
1252
+ kind: "VIOLATED",
1253
+ violation: shrunk.violation,
1254
+ trace: shrunk.trace,
1255
+ schedule: shrunk.schedule,
1256
+ keptEvents: shrunk.keptEvents,
1257
+ shrunk: {
1258
+ faultsRemoved: shrunk.faultsRemoved,
1259
+ eventsRemoved: shrunk.eventsRemoved,
1260
+ runs: shrunk.runs
1261
+ },
1262
+ reached: done()
1263
+ };
1264
+ }
1265
+ }
1266
+ for (const witness of [...witnesses].reverse()) {
1267
+ const again = await replayRun(spec, platform, witness.schedule);
1268
+ reached.runs++;
1269
+ if (again.unsupportedReasons.length > 0 || digestLog(again.log) !== witness.digest) return {
1270
+ ...base,
1271
+ kind: "UNSUPPORTED",
1272
+ cause: "module_state_leak",
1273
+ reason: `run seed ${witness.schedule.seed} produced a different log when replayed after other runs: state is leaking between runs (module-level variables, or a binding Eutaxy does not reset)`,
1274
+ reached: done()
1275
+ };
1276
+ }
1277
+ return {
1278
+ ...base,
1279
+ kind: "BOUNDED",
1280
+ bounds: spec.bounds,
1281
+ reached: done()
1282
+ };
1283
+ }
1284
+ export { Run as a, realTick as c, validateScenario as d, checkInvariant as f, CrashSignal as i, settleOn as l, getPath as m, replay as n, installScriptedFetch as o, checkInvariants as p, restrictSpec as r, patchGlobals as s, explore as t, defaultBounds as u };
1285
+
1286
+ //# sourceMappingURL=src-BrCsF5RZ.mjs.map