@cyberart-io/engine 0.0.2 → 0.0.4

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,805 @@
1
+ /*!
2
+ * @cyberart-io/engine
3
+ * Copyright (c) 2026 Aaron Boyarsky
4
+ * Licensed under the CyberArt Engine License. See LICENSE.
5
+ * Not an OSI open-source license.
6
+ */
7
+ /**
8
+ * Copyright (c) 2026 Aaron Boyarsky
9
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
10
+ * See packages/engine/LICENSE
11
+ *
12
+ * Audio libraries the animation manager knows how to load and unlock.
13
+ * Carts declare which they need via `metadata.audio`; the manager looks them
14
+ * up here. Add a new adapter when a second library is supported.
15
+ */
16
+ type AudioLibraryId = 'tone';
17
+ type AudioLibrarySpec = AudioLibraryId | AudioLibraryId[];
18
+
19
+ /**
20
+ * Copyright (c) 2026 Aaron Boyarsky
21
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
22
+ * See packages/engine/LICENSE
23
+ */
24
+ type ExternalAssetDependency = {
25
+ cid: string;
26
+ [k: string]: unknown;
27
+ };
28
+ type TokenData = {
29
+ hash: string;
30
+ tokenId: string;
31
+ externalAssetDependencies?: ExternalAssetDependency[];
32
+ /**
33
+ * Per AB Engine Flex generator spec: gateway URL the script is supposed to
34
+ * combine with each `externalAssetDependencies[i].cid` of type `"IPFS"`.
35
+ * AB defaults this to `https://ipfs.artblocks.io/ipfs/` and lets partners
36
+ * override per-contract via support. Optional because standard (non-Flex)
37
+ * projects don't get it.
38
+ */
39
+ preferredIPFSGateway?: string;
40
+ /** Same idea for Arweave-typed external assets. We don't currently use it. */
41
+ preferredArweaveGateway?: string;
42
+ };
43
+
44
+ /**
45
+ * Copyright (c) 2026 Aaron Boyarsky
46
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
47
+ * See packages/engine/LICENSE
48
+ */
49
+
50
+ type Sfc32Regs = {
51
+ a: number;
52
+ b: number;
53
+ c: number;
54
+ d: number;
55
+ };
56
+ /** Snapshot of the dual sfc32 generators after warmup (or after `setState`). */
57
+ type RandomState = {
58
+ seed: string;
59
+ useA: boolean;
60
+ prngA: Sfc32Regs;
61
+ prngB: Sfc32Regs;
62
+ };
63
+ declare class Random {
64
+ readonly seed: string;
65
+ private useA;
66
+ private prngA;
67
+ private prngB;
68
+ private genA;
69
+ private genB;
70
+ constructor(tokenData: TokenData);
71
+ getState(): RandomState;
72
+ setState(state: RandomState): void;
73
+ r_zero_one(): number;
74
+ dec(min?: number, max?: number): number;
75
+ int(min: number, max?: number): number;
76
+ bool(p?: number): boolean;
77
+ sign(): 1 | -1;
78
+ choose<T = unknown>(list: T[]): T;
79
+ }
80
+
81
+ type DimensionContext = {
82
+ width: number;
83
+ height: number;
84
+ iWidth: number;
85
+ iHeight: number;
86
+ smallDim: number;
87
+ largeDim: number;
88
+ area: number;
89
+ aspectRatio: number;
90
+ };
91
+
92
+ /**
93
+ * Copyright (c) 2026 Aaron Boyarsky
94
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
95
+ * See packages/engine/LICENSE
96
+ */
97
+ type KeypressHandler = () => void;
98
+ /**
99
+ * Handler for keyboard inputs.
100
+ */
101
+ declare class KeyboardManager {
102
+ private actionMap;
103
+ private debugMode;
104
+ private listening;
105
+ constructor(debugMode?: boolean, captureKeyboard?: boolean);
106
+ /**
107
+ * When a key is pressed, check if there's a corresponding action, and execute it.
108
+ * @param e
109
+ */
110
+ checkKeypress({ key }: KeyboardEvent): void;
111
+ /**
112
+ * Inject a key without a DOM event. Deterministic hosts call this at a
113
+ * chosen frame instead of listening on `window`.
114
+ */
115
+ inject(key: string): void;
116
+ /**
117
+ * Register a certain action to be performed when a given key is pressed.
118
+ * @param key
119
+ * @param action
120
+ * @param overwrite
121
+ */
122
+ registerAction(key: string, action: KeypressHandler, overwrite?: boolean): void;
123
+ /**
124
+ * Remove the global keydown listener and clear registered actions. Call this
125
+ * when the owning animation is torn down so listeners don't accumulate across
126
+ * cart reloads (e.g. on window resize).
127
+ */
128
+ destroy(): void;
129
+ }
130
+
131
+ /**
132
+ * Copyright (c) 2026 Aaron Boyarsky
133
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
134
+ * See packages/engine/LICENSE
135
+ */
136
+ type PointerClick = {
137
+ x: number;
138
+ y: number;
139
+ };
140
+ type PointerManagerOptions = {
141
+ /**
142
+ * When false, do not attach canvas pointer listeners. Deterministic mode
143
+ * injects coordinates instead of reading the live pointer.
144
+ */
145
+ listen?: boolean;
146
+ };
147
+ declare class PointerManager {
148
+ x: number;
149
+ y: number;
150
+ isDown: boolean;
151
+ private clicks;
152
+ private canvas;
153
+ private listening;
154
+ constructor(canvas: HTMLCanvasElement, options?: PointerManagerOptions);
155
+ private toCanvasCoords;
156
+ private onPointerDown;
157
+ private onPointerMove;
158
+ private onPointerUp;
159
+ hasClick(): boolean;
160
+ consumeClick(): PointerClick | null;
161
+ /**
162
+ * Inject a pointer sample in canvas pixel space. Deterministic hosts call
163
+ * this at a chosen frame instead of waiting on DOM pointer events.
164
+ */
165
+ inject(kind: 'down' | 'move' | 'up', x: number, y: number): void;
166
+ destroy(): void;
167
+ }
168
+
169
+ /**
170
+ * Copyright (c) 2026 Aaron Boyarsky
171
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
172
+ * See packages/engine/LICENSE
173
+ *
174
+ * Versioned envelope for routed host ↔ cart events. Unattached HostChannel
175
+ * mailboxes still accept thin `{ type, payload }` events; this module is the
176
+ * schema the EventRouter normalizes to.
177
+ */
178
+ declare const EVENT_ENVELOPE_VERSION: 1;
179
+ type EventKind = 'intent' | 'state' | 'diagnostic';
180
+ type EventEnvelope = {
181
+ schemaVersion: typeof EVENT_ENVELOPE_VERSION;
182
+ type: string;
183
+ kind: EventKind;
184
+ source: string;
185
+ target?: string;
186
+ id: string;
187
+ correlationId: string;
188
+ causationId?: string;
189
+ seq: number;
190
+ hops: number;
191
+ idempotencyKey?: string;
192
+ payload?: unknown;
193
+ };
194
+ type EventInput = {
195
+ type: string;
196
+ payload?: unknown;
197
+ schemaVersion?: number;
198
+ kind?: EventKind;
199
+ source?: string;
200
+ target?: string;
201
+ id?: string;
202
+ correlationId?: string;
203
+ causationId?: string;
204
+ seq?: number;
205
+ hops?: number;
206
+ idempotencyKey?: string;
207
+ };
208
+
209
+ /**
210
+ * Copyright (c) 2026 Aaron Boyarsky
211
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
212
+ * See packages/engine/LICENSE
213
+ *
214
+ * Host ↔ cart mailbox. Inbound events are queued by the host via `dispatch`
215
+ * and drained by cart code that opts in via `consume()`. Outbound events are
216
+ * `emit`ted by the cart and forwarded to host `onEvent` listeners.
217
+ *
218
+ * Carts that never mention this object behave as they do today.
219
+ *
220
+ * Routed traffic is a full EventEnvelope; thin `{ type, payload }` remains
221
+ * valid on an unattached mailbox. Extra envelope fields are optional here.
222
+ */
223
+
224
+ type HostEvent = EventInput;
225
+ type HostEventListener = (event: HostEvent) => void;
226
+ declare class HostChannel {
227
+ private inbound;
228
+ private listeners;
229
+ private closed;
230
+ dispatch(event: HostEvent): void;
231
+ consume(): HostEvent[];
232
+ emit(event: HostEvent): void;
233
+ onEvent(listener: HostEventListener): () => void;
234
+ /** Drop queued inbound events. Does not remove `onEvent` listeners. */
235
+ clearInbound(): void;
236
+ /** Drop inbound events and listeners. Runtime teardown; not cart unload. */
237
+ clear(): void;
238
+ /** Permanent close. Further dispatch / emit / consume / onEvent throw. */
239
+ close(): void;
240
+ private requireOpen;
241
+ }
242
+
243
+ /**
244
+ * Copyright (c) 2026 Aaron Boyarsky
245
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
246
+ * See packages/engine/LICENSE
247
+ */
248
+
249
+ type AnimationTiming = {
250
+ now: number;
251
+ startTime: number;
252
+ elapsedSinceStart: number;
253
+ deltaSinceLastUpdate: number;
254
+ deltaSinceLastRender: number;
255
+ };
256
+ type AnimationCart<T = unknown, TFeatureState = undefined> = {
257
+ getDefaultFeatureState?: (R: Random, dimensionContext: DimensionContext, rawParams: number[], keyboardManager: KeyboardManager, customState?: Partial<T>, pointerManager?: PointerManager, gameManager?: unknown, hostChannel?: HostChannel) => TFeatureState;
258
+ getDefaultState: (R: Random, dimensionContext: DimensionContext, rawParams: number[], keyboardManager: KeyboardManager, customState?: Partial<T>, pointerManager?: PointerManager, gameManager?: unknown, featureState?: Readonly<TFeatureState>, hostChannel?: HostChannel) => T;
259
+ update: (R: Random, framesElapsed: number, rawParams: number[], dimensionContext: DimensionContext, state: T, keyboardManager: KeyboardManager, pointerManager?: PointerManager, gameManager?: unknown, timing?: AnimationTiming, featureState?: Readonly<TFeatureState>, hostChannel?: HostChannel) => T;
260
+ render: (R: Random, framesElapsed: number, rawParams: number[], dimensionContext: DimensionContext, state: T, drawingContext: CanvasRenderingContext2D, imageData: ImageData, pointerManager?: PointerManager, gameManager?: unknown, timing?: AnimationTiming, featureState?: Readonly<TFeatureState>, hostChannel?: HostChannel) => void;
261
+ adjust?: Record<string, {
262
+ type: 'switch';
263
+ immediate?: boolean;
264
+ label: string;
265
+ description: string;
266
+ } | {
267
+ type: 'slider';
268
+ immediate?: boolean;
269
+ description: string;
270
+ min: number;
271
+ max: number;
272
+ step: number;
273
+ label: string;
274
+ mapToOutput: (value: number) => number;
275
+ mapToInput: (value: number) => number;
276
+ }>;
277
+ /**
278
+ * Optional cleanup hook invoked when the cart is unloaded (e.g. on window
279
+ * resize, which fully rebuilds the animation). Carts that allocate long-lived
280
+ * resources such as Tone.js audio nodes should dispose them here to avoid
281
+ * leaks accumulating across reloads.
282
+ */
283
+ teardown?: (state: T, featureState?: Readonly<TFeatureState>) => void;
284
+ metadata?: {
285
+ id: string;
286
+ name: string;
287
+ description?: string;
288
+ frameRate: number;
289
+ /**
290
+ * Optional per-visualMode display FPS overrides (e.g. mycelium at 60
291
+ * while the cart baseline stays lower). Keys match cart `visualMode`.
292
+ */
293
+ frameRateByVisualMode?: Partial<Record<string, number>>;
294
+ /**
295
+ * Audio libraries this cart needs (currently `'tone'`). The animation
296
+ * manager loads and unlocks them; omit or leave unset for a silent cart.
297
+ */
298
+ audio?: AudioLibrarySpec;
299
+ /**
300
+ * When true, outputs are a function of the token hash. Saved state may
301
+ * only be loaded back onto the same hash.
302
+ */
303
+ generative?: boolean;
304
+ };
305
+ };
306
+
307
+ type CartStateDimensions = {
308
+ /** Canvas buffer pixels (device pixels). */
309
+ width: number;
310
+ height: number;
311
+ /** CSS layout size of the window/container at save. */
312
+ cssWidth?: number;
313
+ cssHeight?: number;
314
+ dpr?: number;
315
+ };
316
+ type CartStateBundle = {
317
+ version: number;
318
+ cartId?: string;
319
+ /** True when the cart is hash-seeded; load requires the same seed. */
320
+ generative?: boolean;
321
+ seed: string;
322
+ framesElapsed: number;
323
+ /** Pause-aware animation clock; restored so time-based visuals match the save. */
324
+ elapsedSinceStart?: number;
325
+ /** Output size the simulation was running at. Load pins the canvas to this. */
326
+ dimensions?: CartStateDimensions;
327
+ state: unknown;
328
+ };
329
+
330
+ /**
331
+ * Copyright (c) 2026 Aaron Boyarsky
332
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
333
+ * See packages/engine/LICENSE
334
+ *
335
+ * Host-controlled time, input, and asset completion for deterministic replays.
336
+ * Production kaleidoscope / Art Blocks playback does not enable this mode.
337
+ * Logical asset URLs are resolved by the host preloader (`assetResolver.ts`);
338
+ * this module only times `ASSET_READY_EVENT` / `ASSET_FAILED_EVENT` delivery.
339
+ */
340
+
341
+ type PointerKind = 'down' | 'move' | 'up';
342
+ type ScriptedAction = {
343
+ atFrame: number;
344
+ } & ({
345
+ type: 'pointer';
346
+ pointer: {
347
+ kind: PointerKind;
348
+ x: number;
349
+ y: number;
350
+ };
351
+ } | {
352
+ type: 'key';
353
+ key: string;
354
+ } | {
355
+ type: 'event';
356
+ event: HostEvent;
357
+ } | {
358
+ type: 'asset';
359
+ id: string;
360
+ status: 'ready' | 'failed';
361
+ /** Optional structured failure or resolved resource. Envelope is unchanged. */
362
+ detail?: unknown;
363
+ });
364
+ type DeterministicRuntimeOptions = {
365
+ /** Virtual clock origin in ms. Default 0. */
366
+ origin?: number;
367
+ /** Actions applied at the start of `atFrame`, before `update`. */
368
+ actions?: ScriptedAction[];
369
+ };
370
+ type ClockSnapshot = {
371
+ now: number;
372
+ framesElapsed: number;
373
+ frameRate: number;
374
+ };
375
+ type ReplayMetadata = {
376
+ seed: string;
377
+ clock: ClockSnapshot;
378
+ rng: RandomState;
379
+ actions: ScriptedAction[];
380
+ applied: AppliedAction[];
381
+ events: HostEvent[];
382
+ state: unknown;
383
+ };
384
+ type AppliedAction = {
385
+ frame: number;
386
+ action: ScriptedAction;
387
+ };
388
+
389
+ declare const ASSET_KINDS: readonly ["image", "audio", "font", "spritesheet"];
390
+ type AssetKind = (typeof ASSET_KINDS)[number];
391
+ declare const ASSET_FAILURE_CODES: readonly ["timeout", "cors", "not-found", "invalid", "aborted", "resolver"];
392
+ type AssetFailureCode = (typeof ASSET_FAILURE_CODES)[number];
393
+ type AssetCorsMode = 'anonymous' | 'use-credentials' | 'omit';
394
+ type AssetProvenance = {
395
+ readonly [key: string]: string | number | boolean | null | undefined;
396
+ };
397
+ type AssetDeclaration = {
398
+ /** Cache key and `ASSET_*_EVENT` payload `id`. */
399
+ id: string;
400
+ /** Logical URI: `https://…`, `moltazine:post/<id>#fragment`, `world:asset/…`, `library:…`. */
401
+ ref: string;
402
+ type: AssetKind;
403
+ integrity?: string;
404
+ provenance?: AssetProvenance;
405
+ /** Per-asset timeout. Ignored when wall-clock timeouts are off (deterministic). */
406
+ timeoutMs?: number;
407
+ /**
408
+ * `'silent'` synthesizes `SILENT_ASSET_FALLBACK_REF`. A string is another
409
+ * logical ref of the same type. A declaration is resolved as-is.
410
+ */
411
+ fallback?: 'silent' | string | AssetDeclaration;
412
+ };
413
+ type AssetResolveRequest = {
414
+ id: string;
415
+ ref: string;
416
+ type: AssetKind;
417
+ integrity?: string;
418
+ provenance?: AssetProvenance;
419
+ };
420
+ type ResolvedAsset = {
421
+ id: string;
422
+ ref: string;
423
+ type: AssetKind;
424
+ url: string;
425
+ integrity?: string;
426
+ provenance?: AssetProvenance;
427
+ cors?: AssetCorsMode;
428
+ usedFallback?: boolean;
429
+ /** When true, `dispose` / `forget` call `URL.revokeObjectURL`. */
430
+ managed?: boolean;
431
+ };
432
+ type AssetFailure = {
433
+ id: string;
434
+ ref: string;
435
+ code: AssetFailureCode;
436
+ message: string;
437
+ };
438
+ type AssetItemStatus = {
439
+ state: 'pending';
440
+ } | {
441
+ state: 'loading';
442
+ } | {
443
+ state: 'ready';
444
+ resource: ResolvedAsset;
445
+ failure?: AssetFailure;
446
+ } | {
447
+ state: 'failed';
448
+ failure: AssetFailure;
449
+ };
450
+ type AssetPreloadSnapshot = {
451
+ total: number;
452
+ pending: number;
453
+ ready: number;
454
+ failed: number;
455
+ fallbacks: number;
456
+ items: Record<string, AssetItemStatus>;
457
+ failures: AssetFailure[];
458
+ };
459
+ type AssetResolver = {
460
+ resolve(request: AssetResolveRequest, signal?: AbortSignal): Promise<ResolvedAsset>;
461
+ };
462
+ type AssetRuntimeOptions = {
463
+ resolver: AssetResolver;
464
+ timeoutMs?: number;
465
+ emitEvents?: boolean;
466
+ };
467
+ type AssetPreloader = {
468
+ preload(declarations: readonly AssetDeclaration[]): Promise<AssetPreloadSnapshot>;
469
+ get(id: string): ResolvedAsset | undefined;
470
+ getProgress(): AssetPreloadSnapshot;
471
+ onProgress(listener: (snapshot: AssetPreloadSnapshot) => void): () => void;
472
+ abort(id?: string): void;
473
+ forget(id?: string): void;
474
+ dispose(): void;
475
+ };
476
+
477
+ /**
478
+ * Copyright (c) 2026 Aaron Boyarsky
479
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
480
+ * See packages/engine/LICENSE
481
+ */
482
+
483
+ type FrameErrorInfo = {
484
+ phase: 'update' | 'render' | 'draw';
485
+ consecutive: number;
486
+ stopped: boolean;
487
+ };
488
+ type CreateRuntimeOptions = {
489
+ /** Required mount point. The runtime creates or adopts a canvas inside this element. */
490
+ container: HTMLElement;
491
+ /**
492
+ * Token hash (`0x` + 64 hex) or any seed mixed into one. Instance-local;
493
+ * does not clobber an existing global cache. Kaleidoscope / Art Blocks
494
+ * pass the platform hash unchanged.
495
+ */
496
+ seed?: string | number;
497
+ /**
498
+ * When true, the cart listens for window keydown (full-page players).
499
+ * Defaults to false so an embed does not steal keys from the host.
500
+ * Ignored when `deterministic` is set — input is injected per frame.
501
+ */
502
+ captureKeyboard?: boolean;
503
+ /**
504
+ * Audio libraries to unlock if the host calls `unlockAudio()` before `mount`
505
+ * (click can beat idle prebuild). After mount, `cart.metadata.audio` wins.
506
+ */
507
+ audio?: AudioLibrarySpec;
508
+ /**
509
+ * Host-controlled time, input, and asset events. Production playback
510
+ * (kaleidoscope locally and `build:art`) leaves this unset so rAF and the
511
+ * token hash drive the piece as they do today.
512
+ */
513
+ deterministic?: boolean | DeterministicRuntimeOptions;
514
+ /**
515
+ * Host-pluggable asset resolver/preloader. Carts request logical refs;
516
+ * the host maps them to loadable URLs or blobs. Leave unset when unused.
517
+ * In deterministic mode, preload does not dispatch `ASSET_*` events —
518
+ * scripted `{ type: 'asset' }` actions own delivery timing.
519
+ */
520
+ assets?: AssetRuntimeOptions;
521
+ };
522
+ type MountOptions<T = unknown> = {
523
+ /** Boot overrides passed as `customState` into `getDefaultState`. Not a live-state replay. */
524
+ initialState?: Partial<T>;
525
+ /** Outbound events emitted by cart code via `hostChannel.emit`. */
526
+ onEvent?: HostEventListener;
527
+ /** Site-only. Opaque to the published engine; pass a GameManager from the app. */
528
+ gameManager?: unknown;
529
+ };
530
+ type CartSnapshot = {
531
+ seed: string;
532
+ metadata?: AnimationCart['metadata'];
533
+ pngDataUrl: string;
534
+ };
535
+ type CartHandle = {
536
+ start(): Promise<void>;
537
+ pause(): void;
538
+ resume(): void;
539
+ dispatch(event: HostEvent): void;
540
+ snapshot(): CartSnapshot;
541
+ destroy(): void;
542
+ reload(): void;
543
+ reinit(event?: Event): void;
544
+ /**
545
+ * Live cart state for site chrome (e.g. `/art` debug sliders). Not a public
546
+ * snapshot API — grids and audio nodes are not structured-cloneable.
547
+ */
548
+ getCartState(): unknown;
549
+ exportState(): Promise<CartStateBundle>;
550
+ exportStateJSON(): Promise<string>;
551
+ importState(bundle: CartStateBundle | string, extras?: {
552
+ framebuffer?: ImageData | null;
553
+ }): Promise<void>;
554
+ peekExportedFramebuffer(): ImageData | null;
555
+ peekSeed(): string | undefined;
556
+ isGenerative(): boolean;
557
+ /**
558
+ * Run `frames` ticks on the virtual clock. Requires `deterministic`.
559
+ * Does not use rAF; live kaleidoscope playback never calls this.
560
+ */
561
+ step(frames?: number): Promise<void>;
562
+ /** Run the frames that span `ms` at the cart frame rate. Requires `deterministic`. */
563
+ advance(ms: number): Promise<void>;
564
+ /** Queue a scripted input/asset/host event for a future frame. */
565
+ schedule(action: ScriptedAction): void;
566
+ getClock(): ClockSnapshot;
567
+ getRandomState(): RandomState;
568
+ getReplayMetadata(): Promise<ReplayMetadata>;
569
+ readonly canvas: HTMLCanvasElement | undefined;
570
+ paused: boolean;
571
+ readonly tokenData: TokenData;
572
+ readonly isPrepared: boolean;
573
+ readonly isLoopRunning: boolean;
574
+ readonly needsAudio: boolean;
575
+ readonly audioLibraries: AudioLibraryId[];
576
+ };
577
+ type CyberArtRuntime = {
578
+ mount<T>(cart: AnimationCart<T, any>, options?: MountOptions<T>): CartHandle;
579
+ unlockAudio(): Promise<void>;
580
+ destroy(): void;
581
+ readonly tokenData: TokenData;
582
+ /**
583
+ * This runtime's mailbox. Attach it to `createEventRouter` from the host;
584
+ * carts never receive the router. Survives cart remount; `destroy()` clears it.
585
+ */
586
+ readonly hostChannel: HostChannel;
587
+ /**
588
+ * Preloader for this runtime. Undefined when `assets` was omitted.
589
+ * Survives cart remount; `destroy()` disposes it.
590
+ */
591
+ readonly assets: AssetPreloader | undefined;
592
+ onError?: (error: unknown, info: FrameErrorInfo) => void;
593
+ };
594
+
595
+ /**
596
+ * Copyright (c) 2026 Aaron Boyarsky
597
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
598
+ * See packages/engine/LICENSE
599
+ *
600
+ * CI / agent harness around production `createRuntime({ deterministic })`.
601
+ * `installHeadlessCanvas` is test-only — do not call it from Player or kaleidoscope.
602
+ */
603
+
604
+ /** 1×1 PNG so `captureFrame(path)` writes a file that actually opens. */
605
+ declare const HEADLESS_PNG_DATA_URL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
606
+ declare const DEFAULT_HEADLESS_WIDTH = 320;
607
+ declare const DEFAULT_HEADLESS_HEIGHT = 180;
608
+ /**
609
+ * Documented jsdom canvas install. Mutates `HTMLCanvasElement.prototype`.
610
+ * Idempotent. Not for production playback.
611
+ */
612
+ declare function installHeadlessCanvas(): void;
613
+ type HeadlessFrameError = {
614
+ error: unknown;
615
+ info: FrameErrorInfo;
616
+ };
617
+ type HeadlessInspect = {
618
+ state: unknown;
619
+ events: HostEvent[];
620
+ errors: HeadlessFrameError[];
621
+ replay: ReplayMetadata;
622
+ clock: ClockSnapshot;
623
+ };
624
+ type CreateHeadlessHarnessOptions<T = unknown> = {
625
+ cart: AnimationCart<T>;
626
+ seed?: CreateRuntimeOptions['seed'];
627
+ width?: number;
628
+ height?: number;
629
+ /** Virtual clock origin in ms. Default 0. */
630
+ origin?: number;
631
+ actions?: ScriptedAction[];
632
+ initialState?: Partial<T>;
633
+ gameManager?: unknown;
634
+ onEvent?: HostEventListener;
635
+ onError?: (error: unknown, info: FrameErrorInfo) => void;
636
+ };
637
+ type HeadlessHarness<T = unknown> = {
638
+ readonly runtime: CyberArtRuntime;
639
+ readonly container: HTMLElement;
640
+ readonly events: readonly HostEvent[];
641
+ readonly errors: readonly HeadlessFrameError[];
642
+ readonly cart: CartHandle;
643
+ step(frames?: number): Promise<void>;
644
+ advance(ms: number): Promise<void>;
645
+ schedule(action: ScriptedAction): void;
646
+ dispatch(event: HostEvent): void;
647
+ start(): Promise<void>;
648
+ pause(): void;
649
+ resume(): void;
650
+ readonly paused: boolean;
651
+ key(key: string): void;
652
+ /** Pointer-down at the next frame. Use `schedule` for move/up. Canvas pixels, not CSS. */
653
+ click(x: number, y: number): void;
654
+ inspect(): Promise<HeadlessInspect>;
655
+ captureFrame(path?: string): Promise<CartSnapshot>;
656
+ remount(options?: MountOptions<T>): CartHandle;
657
+ destroy(): void;
658
+ };
659
+ declare function createHeadlessHarness<T>(options: CreateHeadlessHarnessOptions<T>): HeadlessHarness<T>;
660
+
661
+ /**
662
+ * Copyright (c) 2026 Aaron Boyarsky
663
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
664
+ * See packages/engine/LICENSE
665
+ *
666
+ * Host-side event router. Carts keep a dumb HostChannel mailbox; the host
667
+ * attaches those channels here so events are permissioned, budgeted, and
668
+ * loop-checked before they reach another cart. Carts never receive this
669
+ * object.
670
+ */
671
+
672
+ type ValidateResult = true | {
673
+ reason: 'host-rejected';
674
+ detail?: string;
675
+ };
676
+ type AttachOptions = {
677
+ emit?: string[];
678
+ subscribe?: string[];
679
+ authoritative?: boolean;
680
+ };
681
+ type EventRouterOptions = {
682
+ validate?: (event: EventEnvelope) => ValidateResult;
683
+ createId?: () => string;
684
+ now?: () => number;
685
+ hostSource?: string;
686
+ maxPerTurn?: number;
687
+ maxPerWindow?: number;
688
+ windowMs?: number;
689
+ maxCausationDepth?: number;
690
+ maxHops?: number;
691
+ maxCorrelationPerTurn?: number;
692
+ maxIndex?: number;
693
+ };
694
+ type PublishExtras = {
695
+ cause?: EventEnvelope;
696
+ };
697
+ type EventRouter = {
698
+ attach(id: string, channel: HostChannel, options?: AttachOptions): void;
699
+ detach(id: string): void;
700
+ publish(event: EventInput, extras?: PublishExtras): EventEnvelope | undefined;
701
+ subscribe(patterns: string[], listener: (event: EventEnvelope) => void): () => void;
702
+ turn(): void;
703
+ };
704
+
705
+ /**
706
+ * Copyright (c) 2026 Aaron Boyarsky
707
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
708
+ * See packages/engine/LICENSE
709
+ *
710
+ * First-class multi-cart runtime group. Creates production `createRuntime`
711
+ * instances, attaches each mailbox to one shared `createEventRouter`, and
712
+ * locksteps a deterministic clock. Carts never receive the router object.
713
+ */
714
+
715
+ type RuntimeGroupKind = 'render' | 'calculation';
716
+ /**
717
+ * Optional capability-shaped attach hints. Explicit participant `emit` /
718
+ * `subscribe` / `authoritative` win. Do not import the capability manifest
719
+ * module from this file.
720
+ */
721
+ type RuntimeGroupCapability = {
722
+ emit?: string[];
723
+ subscribe?: string[];
724
+ authoritative?: boolean;
725
+ };
726
+ type RuntimeGroupFrameError = {
727
+ error: unknown;
728
+ info: FrameErrorInfo;
729
+ };
730
+ type RuntimeGroupParticipantConfig<T = unknown> = {
731
+ id: string;
732
+ cart: AnimationCart<T>;
733
+ /** Rendered surface vs calculation cart (still a real `AnimationCart`). */
734
+ kind?: RuntimeGroupKind;
735
+ seed?: CreateRuntimeOptions['seed'];
736
+ container?: HTMLElement;
737
+ width?: number;
738
+ height?: number;
739
+ initialState?: Partial<T>;
740
+ gameManager?: unknown;
741
+ onEvent?: HostEventListener;
742
+ emit?: string[];
743
+ subscribe?: string[];
744
+ authoritative?: boolean;
745
+ capability?: RuntimeGroupCapability;
746
+ };
747
+ type CreateRuntimeGroupOptions = {
748
+ participants: RuntimeGroupParticipantConfig[];
749
+ /** Shared virtual-clock origin (ms). Default 0. */
750
+ origin?: number;
751
+ width?: number;
752
+ height?: number;
753
+ validate?: EventRouterOptions['validate'];
754
+ createId?: () => string;
755
+ now?: () => number;
756
+ /** Extra router options. Group injects shared `createId` / `now` unless set here. */
757
+ router?: EventRouterOptions;
758
+ };
759
+ type RuntimeGroupParticipantInspect = {
760
+ state: unknown;
761
+ events: HostEvent[];
762
+ errors: RuntimeGroupFrameError[];
763
+ kind: RuntimeGroupKind;
764
+ clock: ClockSnapshot;
765
+ };
766
+ type RuntimeGroupDiagnostics = {
767
+ paused: boolean;
768
+ participantIds: string[];
769
+ clocks: Record<string, ClockSnapshot>;
770
+ rejections: unknown[];
771
+ };
772
+ type RuntimeGroupInspect = {
773
+ participants: Record<string, RuntimeGroupParticipantInspect>;
774
+ trace: EventEnvelope[];
775
+ diagnostics: RuntimeGroupDiagnostics;
776
+ };
777
+ type RuntimeGroupParticipantHandle = {
778
+ readonly id: string;
779
+ readonly kind: RuntimeGroupKind;
780
+ readonly runtime: CyberArtRuntime;
781
+ readonly container: HTMLElement;
782
+ readonly events: readonly HostEvent[];
783
+ readonly errors: readonly RuntimeGroupFrameError[];
784
+ get cart(): CartHandle;
785
+ };
786
+ type RuntimeGroup = {
787
+ readonly router: EventRouter;
788
+ readonly origin: number;
789
+ readonly paused: boolean;
790
+ participant(id: string): RuntimeGroupParticipantHandle;
791
+ step(frames?: number): Promise<void>;
792
+ pause(): void;
793
+ resume(): void;
794
+ reset(): void;
795
+ dispatch(participantId: string, event: HostEvent): void;
796
+ publish(event: EventInput, extras?: PublishExtras): EventEnvelope | undefined;
797
+ inspect(): Promise<RuntimeGroupInspect>;
798
+ destroy(): void;
799
+ };
800
+
801
+ type CreateHeadlessMultiCartHarnessOptions = CreateRuntimeGroupOptions;
802
+ type HeadlessMultiCartHarness = RuntimeGroup;
803
+ declare function createHeadlessMultiCartHarness(options: CreateHeadlessMultiCartHarnessOptions): HeadlessMultiCartHarness;
804
+
805
+ export { type CreateHeadlessHarnessOptions, type CreateHeadlessMultiCartHarnessOptions, DEFAULT_HEADLESS_HEIGHT, DEFAULT_HEADLESS_WIDTH, HEADLESS_PNG_DATA_URL, type HeadlessFrameError, type HeadlessHarness, type HeadlessInspect, type HeadlessMultiCartHarness, createHeadlessHarness, createHeadlessMultiCartHarness, installHeadlessCanvas };