@mooloo-ai/eutaxy 0.0.0-stage → 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,1091 @@
1
+ import { a as appendEffect, c as canonicalJson, o as digestLog, s as emptyLog } from "./src-Bef00JwK.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 (!/^[a-z0-9][a-z0-9-]*$/.test(spec.name)) errors.push(`name must be lowercase letters, digits and dashes: ${JSON.stringify(spec.name)}`);
315
+ if (!spec.worker.entry) errors.push("worker.entry must not be empty");
316
+ if (!spec.worker.config) errors.push("worker.config must not be empty");
317
+ if (!spec.drive.kind) errors.push("drive.kind must not be empty");
318
+ spec.setup.forEach((s, i) => {
319
+ if (!s.kind) errors.push(`setup[${i}].kind must not be empty`);
320
+ });
321
+ if (spec.invariants.length === 0) errors.push("at least one invariant is required");
322
+ const names = /* @__PURE__ */ new Set();
323
+ for (const inv of spec.invariants) {
324
+ if (!inv.name) errors.push("every invariant needs a name");
325
+ else if (names.has(inv.name)) errors.push(`duplicate invariant name ${JSON.stringify(inv.name)}`);
326
+ names.add(inv.name);
327
+ }
328
+ const ext = /* @__PURE__ */ new Set();
329
+ for (const e of spec.externals) {
330
+ if (ext.has(e.name)) errors.push(`duplicate external ${JSON.stringify(e.name)}`);
331
+ ext.add(e.name);
332
+ }
333
+ for (const k of [
334
+ "maxFaultsPerRun",
335
+ "maxRedeliveries",
336
+ "maxConcurrentInvocations",
337
+ "budgetMs"
338
+ ]) if (k === "maxFaultsPerRun" || k === "maxRedeliveries") {
339
+ const v = spec.bounds[k];
340
+ if (!(Number.isInteger(v) && v >= 0)) errors.push(`bounds.${k} must be a non-negative integer`);
341
+ } else if (!isPositiveInt(spec.bounds[k])) errors.push(`bounds.${k} must be a positive integer`);
342
+ return errors;
343
+ }
344
+ // packages/core/src/shrink.ts
345
+ /** One-step candidates: each single fault removed, then each single event removed. */
346
+ function shrinkCandidates(state) {
347
+ const out = [];
348
+ state.schedule.faults.forEach((_, i) => {
349
+ out.push({
350
+ schedule: {
351
+ seed: state.schedule.seed,
352
+ faults: state.schedule.faults.filter((_, j) => j !== i)
353
+ },
354
+ keepIds: state.keepIds
355
+ });
356
+ });
357
+ if (state.keepIds.length > 1) state.keepIds.forEach((id) => {
358
+ out.push({
359
+ schedule: {
360
+ seed: state.schedule.seed,
361
+ faults: state.schedule.faults.filter((f) => !f.invocation.startsWith(`${id}#`))
362
+ },
363
+ keepIds: state.keepIds.filter((k) => k !== id)
364
+ });
365
+ });
366
+ return out;
367
+ }
368
+ // packages/core/src/verdict.ts
369
+ /** One line per verdict, for error messages; full rendering lives in `report`. */
370
+ function describeVerdict(v) {
371
+ switch (v.kind) {
372
+ case "VIOLATED": return `VIOLATED ${v.scenario}: ${v.violation.message} (seed ${v.schedule.seed}, ${v.reached.runs} run(s))`;
373
+ case "BOUNDED": return `BOUNDED ${v.scenario}: no violation in ${v.reached.runs} run(s), ${v.reached.effectPoints} effect point(s), ${v.reached.faultsInjected} fault(s)`;
374
+ case "UNSUPPORTED": return `UNSUPPORTED ${v.scenario}: ${v.cause}: ${v.reason}`;
375
+ }
376
+ }
377
+ // packages/engine/src/clock.ts
378
+ /**
379
+ * The virtual clock and the globals patch. Within a run, Date, Math.random,
380
+ * crypto.randomUUID, crypto.getRandomValues, performance.now and the timer
381
+ * functions are replaced by deterministic versions driven by the run's seed
382
+ * and by a clock that starts at a fixed epoch and advances only when the
383
+ * engine says so: one millisecond per effect, and to each timer's due time
384
+ * when the timer fires. Timers fire only while the invocation is idle (no
385
+ * effect in flight), in due order, so they never race real I/O.
386
+ */
387
+ /** 2026-01-01T00:00:00.000Z. Every run starts here. */
388
+ const virtualEpoch = Date.UTC(2026, 0, 1);
389
+ const realSetTimeout = globalThis.setTimeout.bind(globalThis);
390
+ var VirtualClock = class {
391
+ time = virtualEpoch;
392
+ timers = [];
393
+ nextId = 1;
394
+ /** The invocation whose code is running; timers scheduled now belong to it. */
395
+ owner = null;
396
+ now() {
397
+ return this.time;
398
+ }
399
+ advance(ms) {
400
+ if (ms > 0) this.time += ms;
401
+ }
402
+ schedule(callback, delay, args, interval) {
403
+ const ms = Number.isFinite(delay) && delay > 0 ? Math.floor(delay) : 0;
404
+ const timer = {
405
+ id: this.nextId++,
406
+ due: this.time + ms,
407
+ callback,
408
+ args,
409
+ interval,
410
+ owner: this.owner
411
+ };
412
+ this.timers.push(timer);
413
+ return timer.id;
414
+ }
415
+ setTimeout(callback, delay = 0, ...args) {
416
+ return this.schedule(callback, delay, args, null);
417
+ }
418
+ setInterval(callback, delay = 0, ...args) {
419
+ return this.schedule(callback, delay, args, Math.max(1, Math.floor(delay)));
420
+ }
421
+ clear(id) {
422
+ this.timers = this.timers.filter((t) => t.id !== id);
423
+ }
424
+ pendingTimers() {
425
+ return this.timers.length;
426
+ }
427
+ /** Drop every timer an invocation scheduled: a crashed invocation's code must never run again. */
428
+ cancelOwner(owner) {
429
+ this.timers = this.timers.filter((t) => t.owner !== owner);
430
+ }
431
+ /** Advance to the earliest due timer and run it. False when none is pending. */
432
+ fireNext() {
433
+ if (this.timers.length === 0) return false;
434
+ let earliest = this.timers[0];
435
+ for (const t of this.timers) if (t.due < earliest.due || t.due === earliest.due && t.id < earliest.id) earliest = t;
436
+ if (earliest.due > this.time) this.time = earliest.due;
437
+ if (earliest.interval === null) this.timers = this.timers.filter((t) => t !== earliest);
438
+ else earliest.due = this.time + earliest.interval;
439
+ try {
440
+ earliest.callback(...earliest.args);
441
+ } catch {}
442
+ return true;
443
+ }
444
+ };
445
+ /** Deterministic randomness for a run: Math.random, UUIDs and random bytes from one seeded stream. */
446
+ var VirtualRandom = class {
447
+ state;
448
+ constructor(seed) {
449
+ this.state = seedPrng(`random:${seed}`);
450
+ }
451
+ float() {
452
+ const [f, next] = nextFloat(this.state);
453
+ this.state = next;
454
+ return f;
455
+ }
456
+ u32() {
457
+ const [u, next] = nextU32(this.state);
458
+ this.state = next;
459
+ return u;
460
+ }
461
+ fill(view) {
462
+ const bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
463
+ for (let i = 0; i < bytes.length; i += 4) {
464
+ const u = this.u32();
465
+ for (let j = 0; j < 4 && i + j < bytes.length; j++) bytes[i + j] = u >>> 8 * j & 255;
466
+ }
467
+ return view;
468
+ }
469
+ /** A version-4 UUID from the stream. */
470
+ uuid() {
471
+ const b = this.fill(/* @__PURE__ */ new Uint8Array(16));
472
+ b[6] = b[6] & 15 | 64;
473
+ b[8] = b[8] & 63 | 128;
474
+ const hex = [...b].map((x) => x.toString(16).padStart(2, "0")).join("");
475
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
476
+ }
477
+ };
478
+ /** Replace the nondeterministic globals for one run. Returns the handle that restores them. */
479
+ function patchGlobals(seed) {
480
+ const clock = new VirtualClock();
481
+ const random = new VirtualRandom(seed);
482
+ const g = globalThis;
483
+ const saved = /* @__PURE__ */ new Map();
484
+ const define = (target, key, value) => {
485
+ const owner = target === g ? g : target;
486
+ saved.set(key, Object.getOwnPropertyDescriptor(owner, key));
487
+ Object.defineProperty(owner, key, {
488
+ value,
489
+ configurable: true,
490
+ writable: true,
491
+ enumerable: false
492
+ });
493
+ };
494
+ const RealDate = Date;
495
+ const VirtualDate = function(...args) {
496
+ if (new.target === void 0) return new RealDate(clock.now()).toString();
497
+ return Reflect.construct(RealDate, args.length === 0 ? [clock.now()] : args, new.target);
498
+ };
499
+ Object.setPrototypeOf(VirtualDate, RealDate);
500
+ Object.defineProperty(VirtualDate, "prototype", {
501
+ value: RealDate.prototype,
502
+ writable: false
503
+ });
504
+ Object.defineProperty(VirtualDate, "now", {
505
+ value: () => clock.now(),
506
+ configurable: true,
507
+ writable: true
508
+ });
509
+ Object.defineProperty(VirtualDate, "name", { value: "Date" });
510
+ define(g, "Date", VirtualDate);
511
+ define(Math, "random", () => random.float());
512
+ define(g, "setTimeout", (cb, ms, ...args) => clock.setTimeout(cb, ms, ...args));
513
+ define(g, "setInterval", (cb, ms, ...args) => clock.setInterval(cb, ms, ...args));
514
+ define(g, "clearTimeout", (id) => id !== void 0 && clock.clear(id));
515
+ define(g, "clearInterval", (id) => id !== void 0 && clock.clear(id));
516
+ const perf = g["performance"];
517
+ if (perf && typeof perf === "object") define(perf, "now", () => clock.now() - virtualEpoch);
518
+ const cryptoObj = g["crypto"];
519
+ if (cryptoObj && typeof cryptoObj === "object") {
520
+ define(cryptoObj, "randomUUID", () => random.uuid());
521
+ define(cryptoObj, "getRandomValues", (view) => random.fill(view));
522
+ }
523
+ let restored = false;
524
+ const targets = {
525
+ Date: g,
526
+ random: Math,
527
+ setTimeout: g,
528
+ setInterval: g,
529
+ clearTimeout: g,
530
+ clearInterval: g
531
+ };
532
+ if (perf && typeof perf === "object") targets["now"] = perf;
533
+ if (cryptoObj && typeof cryptoObj === "object") {
534
+ targets["randomUUID"] = cryptoObj;
535
+ targets["getRandomValues"] = cryptoObj;
536
+ }
537
+ return {
538
+ clock,
539
+ random,
540
+ restore() {
541
+ if (restored) return;
542
+ restored = true;
543
+ for (const [key, descriptor] of saved) {
544
+ const target = targets[key];
545
+ if (descriptor) Object.defineProperty(target, key, descriptor);
546
+ else delete target[key];
547
+ }
548
+ }
549
+ };
550
+ }
551
+ /** Yield to the real event loop so microtasks and pending I/O can make progress. */
552
+ function realTick() {
553
+ return new Promise((resolve) => realSetTimeout(() => resolve(), 0));
554
+ }
555
+ /**
556
+ * Wait for a promise under the virtual clock: while it is pending, let real
557
+ * I/O complete, and when the invocation is idle fire the next due timer.
558
+ */
559
+ async function settleOn(promise, ctx) {
560
+ let done = false;
561
+ let failed = false;
562
+ let value;
563
+ let error;
564
+ const tracked = promise.then((v) => {
565
+ done = true;
566
+ value = v;
567
+ }, (e) => {
568
+ done = true;
569
+ failed = true;
570
+ error = e;
571
+ });
572
+ for (;;) {
573
+ await Promise.race([tracked, realTick()]);
574
+ if (done) {
575
+ if (failed) throw error;
576
+ return value;
577
+ }
578
+ if (ctx.crashed()) return new Promise(() => {});
579
+ if (ctx.inFlight() > 0) continue;
580
+ if (ctx.clock.fireNext()) continue;
581
+ ctx.stuck();
582
+ return new Promise(() => {});
583
+ }
584
+ }
585
+ // packages/engine/src/run.ts
586
+ /**
587
+ * One run of a scenario: the effect log being built, the fault decisions
588
+ * (drawn during exploration, looked up during replay), and the invocations
589
+ * in flight. Proxies in the platform package call `effect`/`effectSync`
590
+ * around every real effect; everything else is engine-internal.
591
+ */
592
+ /** Thrown into user code by a synchronous effect that crashes the invocation. */
593
+ var CrashSignal = class extends Error {
594
+ name = "CrashSignal";
595
+ invocation;
596
+ constructor(invocation) {
597
+ super(`invocation ${invocation} crashed by an injected fault`);
598
+ this.invocation = invocation;
599
+ }
600
+ };
601
+ /** The error user code sees for an injected `error` or `ambiguous` fault. */
602
+ var InjectedEffectError = class extends Error {
603
+ name = "InjectedEffectError";
604
+ point;
605
+ fault;
606
+ constructor(point, fault) {
607
+ super(`${point.target} ${point.op} failed (injected ${fault})`);
608
+ this.point = point;
609
+ this.fault = fault;
610
+ }
611
+ };
612
+ var InvocationState = class {
613
+ crashed;
614
+ onCrash;
615
+ /** Effect points seen so far in this invocation; the next effect's ordinal. */
616
+ ordinal = 0;
617
+ crashedFlag = false;
618
+ resolveCrashed = () => {};
619
+ id;
620
+ constructor(id, onCrash) {
621
+ this.id = id;
622
+ this.onCrash = onCrash;
623
+ this.crashed = new Promise((resolve) => {
624
+ this.resolveCrashed = resolve;
625
+ });
626
+ }
627
+ isCrashed() {
628
+ return this.crashedFlag;
629
+ }
630
+ crash() {
631
+ if (this.crashedFlag) return;
632
+ this.crashedFlag = true;
633
+ this.onCrash();
634
+ this.resolveCrashed();
635
+ }
636
+ };
637
+ /** A promise that never settles: how an abandoned invocation experiences a crash. */
638
+ function hang() {
639
+ return new Promise(() => {});
640
+ }
641
+ var Run = class {
642
+ logState = emptyLog;
643
+ faults = [];
644
+ effectIndex = 0;
645
+ prng;
646
+ invocations = /* @__PURE__ */ new Map();
647
+ reasons = [];
648
+ inFlight = 0;
649
+ patched = null;
650
+ options;
651
+ constructor(options) {
652
+ this.options = options;
653
+ this.prng = seedPrng(options.schedule.seed);
654
+ }
655
+ get log() {
656
+ return this.logState;
657
+ }
658
+ /** The schedule as actually executed: input decisions during replay, drawn ones during exploration. */
659
+ get schedule() {
660
+ return {
661
+ seed: this.options.schedule.seed,
662
+ faults: [...this.faults]
663
+ };
664
+ }
665
+ get effectPoints() {
666
+ return this.effectIndex;
667
+ }
668
+ get faultsInjected() {
669
+ return this.faults.length;
670
+ }
671
+ get unsupportedReasons() {
672
+ return this.reasons;
673
+ }
674
+ /** Replace the nondeterministic globals for this run. Call `uninstall` in a finally. */
675
+ install() {
676
+ if (!this.patched) this.patched = patchGlobals(this.options.schedule.seed);
677
+ }
678
+ uninstall() {
679
+ this.patched?.restore();
680
+ this.patched = null;
681
+ }
682
+ /** Virtual milliseconds since the epoch, or the real clock when the globals are not installed. */
683
+ now() {
684
+ return this.patched ? this.patched.clock.now() : Date.now();
685
+ }
686
+ /** Timers scheduled and not yet fired or cancelled (0 when the globals are not installed). */
687
+ pendingTimers() {
688
+ return this.patched?.clock.pendingTimers() ?? 0;
689
+ }
690
+ startInvocation(id) {
691
+ if (this.invocations.has(id)) throw new Error(`invocation ${id} already started`);
692
+ const inv = new InvocationState(id, () => this.patched?.clock.cancelOwner(id));
693
+ this.invocations.set(id, inv);
694
+ if (this.patched) this.patched.clock.owner = id;
695
+ return inv;
696
+ }
697
+ invocation(id) {
698
+ const inv = this.invocations.get(id);
699
+ if (!inv) throw new Error(`effect from unknown invocation ${id}`);
700
+ return inv;
701
+ }
702
+ decide(point, ordinal) {
703
+ if (this.options.mode === "replay") {
704
+ const d = this.options.schedule.faults.find((f) => f.invocation === point.invocation && f.ordinal === ordinal);
705
+ return d && point.allowed.includes(d.fault) ? d.fault : null;
706
+ }
707
+ const [fault, next] = decideFault(this.prng, point.allowed, this.faults.length, {
708
+ maxFaultsPerRun: this.options.bounds.maxFaultsPerRun,
709
+ faultProbability: this.options.faultProbability
710
+ });
711
+ this.prng = next;
712
+ return fault;
713
+ }
714
+ record(point, kind, payload) {
715
+ this.logState = appendEffect(this.logState, {
716
+ kind,
717
+ invocation: point.invocation,
718
+ target: point.target,
719
+ op: point.op,
720
+ payload
721
+ });
722
+ }
723
+ /** Open an effect point: record the call and decide the fault. */
724
+ open(point, inv) {
725
+ this.effectIndex++;
726
+ const ordinal = inv.ordinal++;
727
+ this.record(point, "call", { args: point.args });
728
+ const fault = this.decide(point, ordinal);
729
+ if (fault) this.faults.push({
730
+ invocation: point.invocation,
731
+ ordinal,
732
+ fault,
733
+ target: point.target,
734
+ op: point.op
735
+ });
736
+ return fault;
737
+ }
738
+ async effect(point, perform, summarize) {
739
+ const inv = this.invocation(point.invocation);
740
+ if (inv.isCrashed()) return hang();
741
+ const fault = this.open(point, inv);
742
+ if (fault === "crash_before" || fault === "cut_off") {
743
+ this.record(point, "fault", {
744
+ args: point.args,
745
+ fault
746
+ });
747
+ inv.crash();
748
+ return hang();
749
+ }
750
+ if (fault === "error") {
751
+ this.record(point, "fault", {
752
+ args: point.args,
753
+ fault
754
+ });
755
+ throw new InjectedEffectError(point, fault);
756
+ }
757
+ let value;
758
+ this.inFlight++;
759
+ try {
760
+ value = await perform();
761
+ } catch (err) {
762
+ this.record(point, "result", {
763
+ args: point.args,
764
+ result: null,
765
+ error: String(err)
766
+ });
767
+ throw err;
768
+ } finally {
769
+ this.inFlight--;
770
+ this.patched?.clock.advance(1);
771
+ }
772
+ this.record(point, "result", {
773
+ args: point.args,
774
+ result: summarize(value)
775
+ });
776
+ if (inv.isCrashed()) return hang();
777
+ if (fault === "crash_after") {
778
+ this.record(point, "fault", {
779
+ args: point.args,
780
+ fault
781
+ });
782
+ inv.crash();
783
+ return hang();
784
+ }
785
+ if (fault === "ambiguous") {
786
+ this.record(point, "fault", {
787
+ args: point.args,
788
+ fault
789
+ });
790
+ throw new InjectedEffectError(point, fault);
791
+ }
792
+ return value;
793
+ }
794
+ effectSync(point, perform, summarize) {
795
+ const inv = this.invocation(point.invocation);
796
+ if (inv.isCrashed()) throw new CrashSignal(inv.id);
797
+ const fault = this.open(point, inv);
798
+ if (fault === "crash_before" || fault === "cut_off") {
799
+ this.record(point, "fault", {
800
+ args: point.args,
801
+ fault
802
+ });
803
+ inv.crash();
804
+ throw new CrashSignal(inv.id);
805
+ }
806
+ if (fault === "error") {
807
+ this.record(point, "fault", {
808
+ args: point.args,
809
+ fault
810
+ });
811
+ throw new InjectedEffectError(point, fault);
812
+ }
813
+ let value;
814
+ try {
815
+ value = perform();
816
+ } catch (err) {
817
+ this.record(point, "result", {
818
+ args: point.args,
819
+ result: null,
820
+ error: String(err)
821
+ });
822
+ throw err;
823
+ }
824
+ this.record(point, "result", {
825
+ args: point.args,
826
+ result: summarize(value)
827
+ });
828
+ if (fault === "crash_after") {
829
+ this.record(point, "fault", {
830
+ args: point.args,
831
+ fault
832
+ });
833
+ inv.crash();
834
+ throw new CrashSignal(inv.id);
835
+ }
836
+ if (fault === "ambiguous") {
837
+ this.record(point, "fault", {
838
+ args: point.args,
839
+ fault
840
+ });
841
+ throw new InjectedEffectError(point, fault);
842
+ }
843
+ return value;
844
+ }
845
+ unsupported(reason) {
846
+ this.reasons.push(reason);
847
+ }
848
+ settle(promise, invocation) {
849
+ const inv = this.invocation(invocation);
850
+ const patched = this.patched;
851
+ if (patched) patched.clock.owner = invocation;
852
+ if (!patched) return Promise.race([promise, inv.crashed.then(() => new Promise(() => {}))]);
853
+ return settleOn(promise, {
854
+ clock: patched.clock,
855
+ inFlight: () => this.inFlight,
856
+ crashed: () => inv.isCrashed(),
857
+ stuck: () => {
858
+ this.unsupported(`invocation ${invocation} is waiting on something Eutaxy cannot see (no effect in flight, no timer due)`);
859
+ inv.crash();
860
+ }
861
+ });
862
+ }
863
+ snapshot(invocation, target, payload) {
864
+ this.logState = appendEffect(this.logState, {
865
+ kind: "snapshot",
866
+ invocation,
867
+ target,
868
+ op: "snapshot",
869
+ payload
870
+ });
871
+ }
872
+ };
873
+ // packages/engine/src/shrink.ts
874
+ /**
875
+ * Shrinking, the loop: try the pure candidates in order, replay each, keep
876
+ * the first that still violates the same invariant for the same keys, and
877
+ * repeat from there until nothing smaller works or the budget is spent.
878
+ */
879
+ /** The scenario restricted to the kept events, with per-message invariants re-derived. */
880
+ function restrictSpec(platform, spec, keepIds) {
881
+ const restricted = platform.restrictEvents(spec, keepIds);
882
+ const bodies = platform.initialEvents(restricted).map((e) => e.data);
883
+ return {
884
+ ...restricted,
885
+ invariants: recomputePerMessageExpected(restricted.invariants, bodies)
886
+ };
887
+ }
888
+ async function shrink(spec, platform, schedule, violation, trace, options) {
889
+ const allIds = platform.initialEvents(spec).map((e) => e.id);
890
+ let state = {
891
+ schedule,
892
+ keepIds: allIds
893
+ };
894
+ let best = {
895
+ trace,
896
+ violation
897
+ };
898
+ let runs = 0;
899
+ let improved = true;
900
+ while (improved && runs < options.maxRuns && options.now() < options.deadline) {
901
+ improved = false;
902
+ for (const candidate of shrinkCandidates(state)) {
903
+ if (runs >= options.maxRuns || options.now() >= options.deadline) break;
904
+ const candidateSpec = restrictSpec(platform, spec, candidate.keepIds);
905
+ const run = await replayRun(candidateSpec, platform, candidate.schedule);
906
+ const log = run.log;
907
+ runs++;
908
+ if (run.unsupportedReasons.length > 0) continue;
909
+ const found = checkInvariants(log, candidateSpec.invariants).find((v) => sameViolation(v, violation));
910
+ if (found) {
911
+ state = candidate;
912
+ best = {
913
+ trace: log,
914
+ violation: found
915
+ };
916
+ improved = true;
917
+ break;
918
+ }
919
+ }
920
+ }
921
+ return {
922
+ spec: restrictSpec(platform, spec, state.keepIds),
923
+ schedule: state.schedule,
924
+ keptEvents: state.keepIds,
925
+ trace: best.trace,
926
+ violation: best.violation,
927
+ runs,
928
+ faultsRemoved: schedule.faults.length - state.schedule.faults.length,
929
+ eventsRemoved: allIds.length - state.keepIds.length
930
+ };
931
+ }
932
+ // packages/engine/src/explore.ts
933
+ /**
934
+ * The exploration loop: run the scenario under seeded fault injection until
935
+ * an invariant breaks or the budget is spent; replay any violation before
936
+ * reporting it.
937
+ */
938
+ const engineAssumptions = [
939
+ "schedule points are effect boundaries: proxied bindings, patched timers and fetch; other awaits run in program order",
940
+ "faults: crash_before, crash_after, ambiguous, error at any effect point; redelivery per the platform's queue model",
941
+ "one invocation at a time (maxConcurrentInvocations = 1)",
942
+ "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",
943
+ "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",
944
+ "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)"
945
+ ];
946
+ /** Run the scenario once under the given run's fault decisions and return its log. */
947
+ async function runOnce(spec, platform, run) {
948
+ run.install();
949
+ try {
950
+ await platform.reset(spec);
951
+ const pending = [...platform.initialEvents(spec)];
952
+ while (pending.length > 0) {
953
+ const event = pending.shift();
954
+ const result = await platform.deliver(event, run, spec);
955
+ pending.push(...result.followUps);
956
+ }
957
+ await platform.finish?.(spec, run);
958
+ } finally {
959
+ run.uninstall();
960
+ }
961
+ return run.log;
962
+ }
963
+ /** Re-run a recorded schedule exactly and return the whole run (log and support status). */
964
+ async function replayRun(spec, platform, schedule) {
965
+ const run = new Run({
966
+ mode: "replay",
967
+ schedule,
968
+ bounds: spec.bounds,
969
+ faultProbability: 0
970
+ });
971
+ await runOnce(spec, platform, run);
972
+ return run;
973
+ }
974
+ /** Re-run a recorded schedule exactly. */
975
+ async function replay(spec, platform, schedule) {
976
+ return (await replayRun(spec, platform, schedule)).log;
977
+ }
978
+ async function explore(spec, platform, options) {
979
+ const now = options.now ?? (() => Date.now());
980
+ const maxRuns = options.maxRuns ?? 100;
981
+ const faultProbability = options.faultProbability ?? .5;
982
+ const assumptions = [...engineAssumptions, ...platform.assumptions];
983
+ const start = now();
984
+ const reached = {
985
+ runs: 0,
986
+ effectPoints: 0,
987
+ faultsInjected: 0
988
+ };
989
+ const done = () => ({
990
+ ...reached,
991
+ elapsedMs: now() - start
992
+ });
993
+ const base = {
994
+ scenario: spec.name,
995
+ seed: options.seed,
996
+ assumptions
997
+ };
998
+ if (spec.bounds.maxConcurrentInvocations !== 1) return {
999
+ ...base,
1000
+ kind: "UNSUPPORTED",
1001
+ cause: "unsupported_bounds",
1002
+ reason: "maxConcurrentInvocations above 1 is not supported yet",
1003
+ reached: done()
1004
+ };
1005
+ const leakCheckRuns = options.leakCheckRuns ?? 2;
1006
+ /** The first runs' schedules and digests, replayed in reverse order before BOUNDED. */
1007
+ const witnesses = [];
1008
+ let master = seedPrng(options.seed);
1009
+ while (reached.runs < maxRuns && now() - start < spec.bounds.budgetMs) {
1010
+ const [runSeed, next] = nextU32(master);
1011
+ master = next;
1012
+ const run = new Run({
1013
+ mode: "explore",
1014
+ schedule: {
1015
+ seed: runSeed,
1016
+ faults: []
1017
+ },
1018
+ bounds: spec.bounds,
1019
+ faultProbability
1020
+ });
1021
+ const log = await runOnce(spec, platform, run);
1022
+ reached.runs++;
1023
+ reached.effectPoints += run.effectPoints;
1024
+ reached.faultsInjected += run.faultsInjected;
1025
+ if (witnesses.length < leakCheckRuns) witnesses.push({
1026
+ schedule: run.schedule,
1027
+ digest: digestLog(log)
1028
+ });
1029
+ if (run.unsupportedReasons.length > 0) return {
1030
+ ...base,
1031
+ kind: "UNSUPPORTED",
1032
+ cause: "unsupported_api",
1033
+ reason: run.unsupportedReasons.join("; "),
1034
+ reached: done()
1035
+ };
1036
+ const violation = checkInvariants(log, spec.invariants)[0];
1037
+ if (violation) {
1038
+ const schedule = run.schedule;
1039
+ const replayedRun = await replayRun(spec, platform, schedule);
1040
+ const replayed = replayedRun.log;
1041
+ reached.runs++;
1042
+ if (replayedRun.unsupportedReasons.length > 0 || digestLog(replayed) !== digestLog(log)) return {
1043
+ ...base,
1044
+ kind: "UNSUPPORTED",
1045
+ cause: "not_replayable",
1046
+ reason: `the violating run of seed ${schedule.seed} did not reproduce on replay`,
1047
+ reached: done()
1048
+ };
1049
+ const shrunk = await shrink(spec, platform, schedule, violation, replayed, {
1050
+ maxRuns: options.maxShrinkRuns ?? 200,
1051
+ deadline: now() + spec.bounds.budgetMs,
1052
+ now
1053
+ });
1054
+ reached.runs += shrunk.runs;
1055
+ return {
1056
+ ...base,
1057
+ kind: "VIOLATED",
1058
+ violation: shrunk.violation,
1059
+ trace: shrunk.trace,
1060
+ schedule: shrunk.schedule,
1061
+ keptEvents: shrunk.keptEvents,
1062
+ shrunk: {
1063
+ faultsRemoved: shrunk.faultsRemoved,
1064
+ eventsRemoved: shrunk.eventsRemoved,
1065
+ runs: shrunk.runs
1066
+ },
1067
+ reached: done()
1068
+ };
1069
+ }
1070
+ }
1071
+ for (const witness of [...witnesses].reverse()) {
1072
+ const again = await replayRun(spec, platform, witness.schedule);
1073
+ reached.runs++;
1074
+ if (again.unsupportedReasons.length > 0 || digestLog(again.log) !== witness.digest) return {
1075
+ ...base,
1076
+ kind: "UNSUPPORTED",
1077
+ cause: "module_state_leak",
1078
+ 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)`,
1079
+ reached: done()
1080
+ };
1081
+ }
1082
+ return {
1083
+ ...base,
1084
+ kind: "BOUNDED",
1085
+ bounds: spec.bounds,
1086
+ reached: done()
1087
+ };
1088
+ }
1089
+ export { Run as a, describeVerdict as c, checkInvariant as d, checkInvariants as f, CrashSignal as i, defaultBounds as l, replay as n, patchGlobals as o, getPath as p, restrictSpec as r, settleOn as s, explore as t, validateScenario as u };
1090
+
1091
+ //# sourceMappingURL=src-Dn3Lm5Us.mjs.map