@cyberart-io/engine 0.0.4 → 0.0.6

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.
@@ -177,6 +177,7 @@ declare class PointerManager {
177
177
  */
178
178
  declare const EVENT_ENVELOPE_VERSION: 1;
179
179
  type EventKind = 'intent' | 'state' | 'diagnostic';
180
+ type RejectionReason = 'malformed' | 'unauthorized' | 'host-rejected' | 'rate-limited' | 'loop-detected' | 'storm-detected' | 'unknown-target' | 'not-subscribed' | 'hop-limit';
180
181
  type EventEnvelope = {
181
182
  schemaVersion: typeof EVENT_ENVELOPE_VERSION;
182
183
  type: string;
@@ -327,6 +328,76 @@ type CartStateBundle = {
327
328
  state: unknown;
328
329
  };
329
330
 
331
+ /**
332
+ * Copyright (c) 2026 Aaron Boyarsky
333
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
334
+ * See packages/engine/LICENSE
335
+ *
336
+ * Versioned snapshot envelope. Schema 1 is the previous engine-owned
337
+ * `CartStateBundle` (`CART_STATE_BUNDLE_VERSION = 1`). Schema 2 wraps that
338
+ * blob in `engineState` and separates host-owned payload. Hosts persist
339
+ * the JSON; this module is not a database.
340
+ */
341
+
342
+ /**
343
+ * Current envelope schema. Independent of `CART_STATE_BUNDLE_VERSION` (still
344
+ * 1 inside `engineState`) and of the npm package version.
345
+ */
346
+ declare const SNAPSHOT_SCHEMA_VERSION: 2;
347
+ type SnapshotCartRef = {
348
+ id: string;
349
+ version: string;
350
+ generative?: boolean;
351
+ };
352
+ type SnapshotModuleRef = {
353
+ id: string;
354
+ version: string;
355
+ };
356
+ type SnapshotClock = {
357
+ framesElapsed: number;
358
+ elapsedSinceStart?: number;
359
+ now?: number;
360
+ frameRate?: number;
361
+ };
362
+ type SnapshotAssetRef = {
363
+ id: string;
364
+ version?: string;
365
+ ref?: string;
366
+ };
367
+ type SnapshotIntegrity = {
368
+ alg: string;
369
+ hash: string;
370
+ };
371
+ type SnapshotProvenance = {
372
+ source?: string;
373
+ integrity?: SnapshotIntegrity;
374
+ };
375
+ type SnapshotEnvelope = {
376
+ schemaVersion: typeof SNAPSHOT_SCHEMA_VERSION;
377
+ runtimeVersion: string;
378
+ cart: SnapshotCartRef;
379
+ seed: string;
380
+ clock: SnapshotClock;
381
+ engineState: CartStateBundle;
382
+ createdAt: string;
383
+ modules?: SnapshotModuleRef[];
384
+ rng?: RandomState;
385
+ hostState?: unknown;
386
+ hostStateRef?: string;
387
+ assets?: SnapshotAssetRef[];
388
+ provenance?: SnapshotProvenance;
389
+ };
390
+ type ExportSnapshotOptions = {
391
+ cartVersion?: string;
392
+ modules?: SnapshotModuleRef[];
393
+ hostState?: unknown;
394
+ hostStateRef?: string;
395
+ assets?: SnapshotAssetRef[];
396
+ createdAt?: string;
397
+ provenance?: SnapshotProvenance;
398
+ runtimeVersion?: string;
399
+ };
400
+
330
401
  /**
331
402
  * Copyright (c) 2026 Aaron Boyarsky
332
403
  * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
@@ -474,6 +545,75 @@ type AssetPreloader = {
474
545
  dispose(): void;
475
546
  };
476
547
 
548
+ type AudioUnlockState = 'locked' | 'unlocking' | 'unlocked' | 'failed';
549
+ type AudioUnlockStatus = {
550
+ state: AudioUnlockState;
551
+ error?: string;
552
+ };
553
+ type AudioAssetStatus = 'ready' | 'failed';
554
+ type AudioChannelInspect = {
555
+ id: string;
556
+ participantId?: string;
557
+ gain: number;
558
+ muted: boolean;
559
+ duckGain: number;
560
+ priority: number;
561
+ effectiveGain: number;
562
+ };
563
+ type AudioBrokerInspect = {
564
+ status: AudioUnlockStatus;
565
+ reducedSensory: boolean;
566
+ muted: boolean;
567
+ authorized: string[];
568
+ channels: AudioChannelInspect[];
569
+ assets: Record<string, AudioAssetStatus>;
570
+ };
571
+ type AudioBrokerNotice = {
572
+ type: 'asset';
573
+ id: string;
574
+ status: AudioAssetStatus;
575
+ } | {
576
+ type: 'teardown';
577
+ participantId: string;
578
+ } | {
579
+ type: 'mute';
580
+ } | {
581
+ type: 'destroy';
582
+ };
583
+ type AudioBrokerListener = (notice: AudioBrokerNotice) => void;
584
+ type ActiveAudioCue = {
585
+ idempotencyKey: string;
586
+ channelId: string;
587
+ participantId?: string;
588
+ priority: number;
589
+ };
590
+ type AudioBroker = {
591
+ unlock(): Promise<AudioUnlockStatus>;
592
+ status(): AudioUnlockStatus;
593
+ authorize(participantId: string): void;
594
+ revoke(participantId: string): void;
595
+ isAuthorized(participantId: string | undefined): boolean;
596
+ setChannelGain(channelId: string, gain: number, participantId?: string): void;
597
+ setPriority(channelId: string, priority: number, participantId?: string): void;
598
+ mute(): void;
599
+ unmute(): void;
600
+ muteChannel(channelId: string, participantId?: string): void;
601
+ unmuteChannel(channelId: string): void;
602
+ duck(channelId: string, gain?: number): void;
603
+ unduck(channelId: string): void;
604
+ effectiveGain(channelId: string): number;
605
+ handleHostEvent(event: HostEvent): void;
606
+ assetStatus(id: string): AudioAssetStatus | undefined;
607
+ noteCueStarted(cue: ActiveAudioCue): void;
608
+ noteCueEnded(idempotencyKey: string): void;
609
+ teardown(participantId: string): void;
610
+ onNotice(listener: AudioBrokerListener): () => void;
611
+ inspect(): AudioBrokerInspect;
612
+ destroy(): void;
613
+ readonly reducedSensory: boolean;
614
+ readonly muted: boolean;
615
+ };
616
+
477
617
  /**
478
618
  * Copyright (c) 2026 Aaron Boyarsky
479
619
  * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
@@ -518,6 +658,17 @@ type CreateRuntimeOptions = {
518
658
  * scripted `{ type: 'asset' }` actions own delivery timing.
519
659
  */
520
660
  assets?: AssetRuntimeOptions;
661
+ /**
662
+ * Shared page-level unlock broker. Optional. Carts that only set
663
+ * `metadata.audio: 'tone'` keep the existing `start()` / `unlockAudio()`
664
+ * path when this is omitted.
665
+ */
666
+ audioBroker?: AudioBroker;
667
+ /**
668
+ * Runtime-group participant id to authorize on this runtime. Teardown of
669
+ * this id does not close Tone for remaining carts.
670
+ */
671
+ audioParticipantId?: string;
521
672
  };
522
673
  type MountOptions<T = unknown> = {
523
674
  /** Boot overrides passed as `customState` into `getDefaultState`. Not a live-state replay. */
@@ -548,7 +699,12 @@ type CartHandle = {
548
699
  getCartState(): unknown;
549
700
  exportState(): Promise<CartStateBundle>;
550
701
  exportStateJSON(): Promise<string>;
551
- importState(bundle: CartStateBundle | string, extras?: {
702
+ importState(bundle: CartStateBundle | SnapshotEnvelope | string, extras?: {
703
+ framebuffer?: ImageData | null;
704
+ }): Promise<void>;
705
+ exportSnapshot(options?: ExportSnapshotOptions): Promise<SnapshotEnvelope>;
706
+ exportSnapshotJSON(options?: ExportSnapshotOptions): Promise<string>;
707
+ importSnapshot(input: SnapshotEnvelope | CartStateBundle | string, extras?: {
552
708
  framebuffer?: ImageData | null;
553
709
  }): Promise<void>;
554
710
  peekExportedFramebuffer(): ImageData | null;
@@ -589,75 +745,11 @@ type CyberArtRuntime = {
589
745
  * Survives cart remount; `destroy()` disposes it.
590
746
  */
591
747
  readonly assets: AssetPreloader | undefined;
748
+ /** Shared broker when `createRuntime({ audioBroker })` was set. */
749
+ readonly audioBroker: AudioBroker | undefined;
592
750
  onError?: (error: unknown, info: FrameErrorInfo) => void;
593
751
  };
594
752
 
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
753
  /**
662
754
  * Copyright (c) 2026 Aaron Boyarsky
663
755
  * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
@@ -690,15 +782,49 @@ type EventRouterOptions = {
690
782
  maxHops?: number;
691
783
  maxCorrelationPerTurn?: number;
692
784
  maxIndex?: number;
785
+ /** Bound on the decision trace (oldest dropped). Default 256. */
786
+ maxDecisions?: number;
693
787
  };
694
788
  type PublishExtras = {
695
789
  cause?: EventEnvelope;
696
790
  };
791
+ type RouterParticipantInspect = {
792
+ id: string;
793
+ emit: string[];
794
+ subscribe: string[];
795
+ authoritative: boolean;
796
+ };
797
+ type RouterDecisionOutcome = 'accepted' | 'rejected' | 'duplicate';
798
+ type RouterDecisionReason = RejectionReason | 'duplicate';
799
+ type RouterDecision = {
800
+ outcome: RouterDecisionOutcome;
801
+ reason?: RouterDecisionReason;
802
+ detail?: string;
803
+ source: string;
804
+ type: string;
805
+ kind?: EventKind;
806
+ envelopeId?: string;
807
+ priorEnvelopeId?: string;
808
+ target?: string;
809
+ deliveredTo: string[];
810
+ hops?: number;
811
+ seq?: number;
812
+ correlationId?: string;
813
+ causationId?: string;
814
+ idempotencyKey?: string;
815
+ schemaVersion?: number;
816
+ payload?: unknown;
817
+ turn: number;
818
+ time: number;
819
+ };
697
820
  type EventRouter = {
698
821
  attach(id: string, channel: HostChannel, options?: AttachOptions): void;
699
822
  detach(id: string): void;
700
823
  publish(event: EventInput, extras?: PublishExtras): EventEnvelope | undefined;
701
824
  subscribe(patterns: string[], listener: (event: EventEnvelope) => void): () => void;
825
+ subscribeDecision(listener: (decision: RouterDecision) => void): () => void;
826
+ inspectParticipants(): RouterParticipantInspect[];
827
+ inspectDecisions(): RouterDecision[];
702
828
  turn(): void;
703
829
  };
704
830
 
@@ -755,6 +881,10 @@ type CreateRuntimeGroupOptions = {
755
881
  now?: () => number;
756
882
  /** Extra router options. Group injects shared `createId` / `now` unless set here. */
757
883
  router?: EventRouterOptions;
884
+ /** Bound on the accepted-event trace (oldest dropped). Default 1024. */
885
+ maxTrace?: number;
886
+ /** Shared page-level audio unlock broker. Optional. */
887
+ audioBroker?: AudioBroker;
758
888
  };
759
889
  type RuntimeGroupParticipantInspect = {
760
890
  state: unknown;
@@ -762,6 +892,9 @@ type RuntimeGroupParticipantInspect = {
762
892
  errors: RuntimeGroupFrameError[];
763
893
  kind: RuntimeGroupKind;
764
894
  clock: ClockSnapshot;
895
+ emit: string[];
896
+ subscribe: string[];
897
+ authoritative: boolean;
765
898
  };
766
899
  type RuntimeGroupDiagnostics = {
767
900
  paused: boolean;
@@ -787,19 +920,597 @@ type RuntimeGroup = {
787
920
  readonly router: EventRouter;
788
921
  readonly origin: number;
789
922
  readonly paused: boolean;
923
+ readonly audioBroker: AudioBroker | undefined;
790
924
  participant(id: string): RuntimeGroupParticipantHandle;
791
925
  step(frames?: number): Promise<void>;
792
926
  pause(): void;
793
927
  resume(): void;
794
928
  reset(): void;
929
+ /** Shared viewport. Sets each container and canvas buffer size. Does not clear sibling pixels on detach. */
930
+ resize(width: number, height: number): void;
931
+ /** Tear down one participant without destroying the group or blanking siblings. */
932
+ detach(id: string): void;
795
933
  dispatch(participantId: string, event: HostEvent): void;
796
934
  publish(event: EventInput, extras?: PublishExtras): EventEnvelope | undefined;
935
+ inspectParticipants(): Array<RouterParticipantInspect & {
936
+ kind: RuntimeGroupKind;
937
+ }>;
797
938
  inspect(): Promise<RuntimeGroupInspect>;
798
939
  destroy(): void;
799
940
  };
800
941
 
942
+ declare const COMPOSITOR_BLEND_MODES: readonly ["source-over", "screen"];
943
+ type CompositorBlendMode = (typeof COMPOSITOR_BLEND_MODES)[number];
944
+ declare const COMPOSITOR_CLEAR_POLICIES: readonly ["transparent", "opaque"];
945
+ type CompositorClearPolicy = (typeof COMPOSITOR_CLEAR_POLICIES)[number];
946
+ type CompositorPointerEvents = 'auto' | 'none';
947
+ type CompositorClip = {
948
+ x: number;
949
+ y: number;
950
+ width: number;
951
+ height: number;
952
+ };
953
+ type CompositorLayerConfig = {
954
+ id: string;
955
+ order: number;
956
+ visible?: boolean;
957
+ opacity?: number;
958
+ blend?: CompositorBlendMode;
959
+ clip?: CompositorClip;
960
+ pointerEvents?: CompositorPointerEvents;
961
+ clearPolicy?: CompositorClearPolicy;
962
+ };
963
+ type CompositorLayerInspect = {
964
+ id: string;
965
+ order: number;
966
+ visible: boolean;
967
+ opacity: number;
968
+ blend: CompositorBlendMode;
969
+ clip: CompositorClip | null;
970
+ pointerEvents: CompositorPointerEvents;
971
+ clearPolicy: CompositorClearPolicy;
972
+ };
973
+ type ComposedFrame = {
974
+ imageData: ImageData;
975
+ pngDataUrl: string;
976
+ width: number;
977
+ height: number;
978
+ declaredOrder: CompositorLayerInspect[];
979
+ };
980
+ type CompositorInspect = {
981
+ viewport: {
982
+ width: number;
983
+ height: number;
984
+ dpr: number;
985
+ };
986
+ clearPolicy: CompositorClearPolicy;
987
+ layers: CompositorLayerInspect[];
988
+ declaredOrder: string[];
989
+ };
990
+ type Compositor = {
991
+ readonly canvas: HTMLCanvasElement;
992
+ readonly width: number;
993
+ readonly height: number;
994
+ readonly dpr: number;
995
+ compose(): ImageData;
996
+ captureComposedFrame(): ComposedFrame;
997
+ /**
998
+ * Shared CSS viewport. Updates group canvas buffer size (`width * dpr`).
999
+ * Layer order/blend survive. Carts that cache layout from `DimensionContext`
1000
+ * need `group.reset()` (or remount) so they rebuild at the new size.
1001
+ */
1002
+ resize(width: number, height: number, dpr?: number): void;
1003
+ setLayer(id: string, patch: Partial<Omit<CompositorLayerConfig, 'id'>>): void;
1004
+ layer(id: string): CompositorLayerInspect;
1005
+ layers(): CompositorLayerInspect[];
1006
+ pointerTarget(x: number, y: number): string | undefined;
1007
+ unmountLayer(id: string): void;
1008
+ inspect(): CompositorInspect;
1009
+ destroy(): void;
1010
+ };
1011
+
1012
+ /**
1013
+ * Copyright (c) 2026 Aaron Boyarsky
1014
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
1015
+ * See packages/engine/LICENSE
1016
+ *
1017
+ * Deterministic software Canvas2D for jsdom / CI. Integer pixel coverage
1018
+ * (pixel-center samples, nearest-neighbor images, no antialiasing). No
1019
+ * Math.random. Unsupported operations throw HeadlessUnsupportedOperationError
1020
+ * instead of no-op.
1021
+ */
1022
+ declare class HeadlessUnsupportedOperationError extends Error {
1023
+ readonly operation: string;
1024
+ name: string;
1025
+ constructor(operation: string, detail?: string);
1026
+ }
1027
+ type GlyphAtlas = {
1028
+ cellWidth: number;
1029
+ cellHeight: number;
1030
+ /** Packed 0/1 coverage, `cellWidth * cellHeight` bits per glyph, row-major. */
1031
+ glyphs: Record<string, Uint8Array>;
1032
+ };
1033
+ type HeadlessImageFixture = {
1034
+ readonly __headlessImage: true;
1035
+ width: number;
1036
+ height: number;
1037
+ data: Uint8ClampedArray;
1038
+ };
1039
+ type HeadlessCanvas2DSettings = {
1040
+ alpha?: boolean;
1041
+ glyphAtlas?: GlyphAtlas;
1042
+ };
1043
+ declare class HeadlessSurface {
1044
+ readonly canvas: HTMLCanvasElement;
1045
+ width: number;
1046
+ height: number;
1047
+ readonly alpha: boolean;
1048
+ pixels: Uint8ClampedArray;
1049
+ glyphAtlas: GlyphAtlas;
1050
+ context: CanvasRenderingContext2D | null;
1051
+ onDeviceReset: (() => void) | null;
1052
+ constructor(canvas: HTMLCanvasElement, width: number, height: number, settings: HeadlessCanvas2DSettings);
1053
+ resize(width: number, height: number, clear?: boolean): void;
1054
+ fillOpaqueBlack(): void;
1055
+ }
1056
+ declare function createImageFixture(width: number, height: number, data?: Uint8ClampedArray): HeadlessImageFixture;
1057
+ declare function setDefaultGlyphAtlas(atlas: GlyphAtlas | undefined): void;
1058
+ declare function getHeadlessSurface(canvas: HTMLCanvasElement): HeadlessSurface | undefined;
1059
+ declare function attachHeadlessCanvas2D(canvas: HTMLCanvasElement, settings?: HeadlessCanvas2DSettings): CanvasRenderingContext2D;
1060
+ declare function makeImageData(data: Uint8ClampedArray, width: number, height: number): ImageData;
1061
+ declare function createDefaultGlyphAtlas(): GlyphAtlas;
1062
+ declare function encodePng(width: number, height: number, pixels: Uint8ClampedArray | Uint8Array): Uint8Array;
1063
+ declare function encodePngDataUrl(width: number, height: number, pixels: Uint8ClampedArray | Uint8Array): string;
1064
+ declare function decodePng(bytes: Uint8Array): ImageData;
1065
+
1066
+ /**
1067
+ * Copyright (c) 2026 Aaron Boyarsky
1068
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
1069
+ * See packages/engine/LICENSE
1070
+ *
1071
+ * CI / agent harness around production `createRuntime({ deterministic })`.
1072
+ * `installHeadlessCanvas` is test-only — do not call it from Player or kaleidoscope.
1073
+ */
1074
+
1075
+ /** 1×1 PNG fallback when `toDataURL` runs before a 2D context is attached. */
1076
+ declare const HEADLESS_PNG_DATA_URL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
1077
+ declare const DEFAULT_HEADLESS_WIDTH = 320;
1078
+ declare const DEFAULT_HEADLESS_HEIGHT = 180;
1079
+ type InstallHeadlessCanvasOptions = {
1080
+ glyphAtlas?: GlyphAtlas;
1081
+ };
1082
+ /**
1083
+ * Documented jsdom canvas install. Mutates `HTMLCanvasElement.prototype`.
1084
+ * Idempotent. Not for production playback. Software Canvas2D pixels flow
1085
+ * through `getContext` / `toDataURL` so `cart.snapshot()` is the real frame.
1086
+ */
1087
+ declare function installHeadlessCanvas(options?: InstallHeadlessCanvasOptions): void;
1088
+ type HeadlessFrameError = {
1089
+ error: unknown;
1090
+ info: FrameErrorInfo;
1091
+ };
1092
+ type HeadlessInspect = {
1093
+ state: unknown;
1094
+ events: HostEvent[];
1095
+ errors: HeadlessFrameError[];
1096
+ replay: ReplayMetadata;
1097
+ clock: ClockSnapshot;
1098
+ };
1099
+ type CreateHeadlessHarnessOptions<T = unknown> = {
1100
+ cart: AnimationCart<T>;
1101
+ seed?: CreateRuntimeOptions['seed'];
1102
+ width?: number;
1103
+ height?: number;
1104
+ /** Virtual clock origin in ms. Default 0. */
1105
+ origin?: number;
1106
+ actions?: ScriptedAction[];
1107
+ initialState?: Partial<T>;
1108
+ gameManager?: unknown;
1109
+ onEvent?: HostEventListener;
1110
+ onError?: (error: unknown, info: FrameErrorInfo) => void;
1111
+ };
1112
+ type HeadlessHarness<T = unknown> = {
1113
+ readonly runtime: CyberArtRuntime;
1114
+ readonly container: HTMLElement;
1115
+ readonly events: readonly HostEvent[];
1116
+ readonly errors: readonly HeadlessFrameError[];
1117
+ readonly cart: CartHandle;
1118
+ step(frames?: number): Promise<void>;
1119
+ advance(ms: number): Promise<void>;
1120
+ schedule(action: ScriptedAction): void;
1121
+ dispatch(event: HostEvent): void;
1122
+ start(): Promise<void>;
1123
+ pause(): void;
1124
+ resume(): void;
1125
+ readonly paused: boolean;
1126
+ key(key: string): void;
1127
+ /** Pointer-down at the next frame. Use `schedule` for move/up. Canvas pixels, not CSS. */
1128
+ click(x: number, y: number): void;
1129
+ inspect(): Promise<HeadlessInspect>;
1130
+ captureFrame(path?: string): Promise<CartSnapshot>;
1131
+ remount(options?: MountOptions<T>): CartHandle;
1132
+ destroy(): void;
1133
+ };
1134
+ declare function createHeadlessHarness<T>(options: CreateHeadlessHarnessOptions<T>): HeadlessHarness<T>;
1135
+
1136
+ declare const UPDATE_GOLDEN_ENV = "CYBERART_UPDATE_GOLDEN";
1137
+ type VisualCompareOptions = {
1138
+ /** Max per-channel absolute delta (0 = exact). */
1139
+ tolerance?: number;
1140
+ /** Max allowed differing pixels after tolerance. */
1141
+ maxDifferingPixels?: number;
1142
+ /**
1143
+ * When set, a pixel matches if weighted RGB distance <= this value.
1144
+ * Weights are 0.299 / 0.587 / 0.114 (documented luma).
1145
+ */
1146
+ perceptualThreshold?: number;
1147
+ /** Alpha channel max delta. Defaults to `tolerance` (including when perceptual RGB is used). */
1148
+ alphaTolerance?: number;
1149
+ /** Force golden-update behavior regardless of env. */
1150
+ updateGolden?: boolean;
1151
+ };
1152
+ type VisualArtifacts = {
1153
+ expectedPng: Uint8Array;
1154
+ actualPng: Uint8Array;
1155
+ diffPng: Uint8Array;
1156
+ expectedDataUrl: string;
1157
+ actualDataUrl: string;
1158
+ diffDataUrl: string;
1159
+ differingPixels: number;
1160
+ };
1161
+ type VisualCompareResult = {
1162
+ match: boolean;
1163
+ differingPixels: number;
1164
+ width: number;
1165
+ height: number;
1166
+ artifacts: VisualArtifacts;
1167
+ updatedGolden: boolean;
1168
+ };
1169
+ declare function shouldUpdateGolden(env?: {
1170
+ [key: string]: string | undefined;
1171
+ } | undefined): boolean;
1172
+ declare function imageDataFromPngDataUrl(dataUrl: string): ImageData;
1173
+ declare function compareImageData(actual: ImageData, expected: ImageData, options?: VisualCompareOptions): VisualCompareResult;
1174
+ declare function assertPixelsEqual(actual: ImageData, expected: ImageData, options?: VisualCompareOptions): VisualCompareResult;
1175
+ declare function assertPngDataUrlsEqual(actualDataUrl: string, expectedDataUrl: string, options?: VisualCompareOptions): VisualCompareResult;
1176
+ type VisualArtifactPaths = {
1177
+ expectedPath: string;
1178
+ actualPath: string;
1179
+ diffPath: string;
1180
+ };
1181
+ /**
1182
+ * Node-only. Writes expected.png / actual.png / diff.png under `dir`.
1183
+ * Uses a dynamic `node:fs/promises` import so the browser runtime graph stays clean.
1184
+ */
1185
+ declare function writeVisualArtifacts(dir: string, artifacts: VisualArtifacts, names?: {
1186
+ expected?: string;
1187
+ actual?: string;
1188
+ diff?: string;
1189
+ }): Promise<VisualArtifactPaths>;
1190
+
801
1191
  type CreateHeadlessMultiCartHarnessOptions = CreateRuntimeGroupOptions;
802
1192
  type HeadlessMultiCartHarness = RuntimeGroup;
803
1193
  declare function createHeadlessMultiCartHarness(options: CreateHeadlessMultiCartHarnessOptions): HeadlessMultiCartHarness;
804
1194
 
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 };
1195
+ /**
1196
+ * Copyright (c) 2026 Aaron Boyarsky
1197
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
1198
+ * See packages/engine/LICENSE
1199
+ *
1200
+ * One contract definition drives TypeScript payload types, runtime
1201
+ * validation, router permission checks, and machine-readable manifests.
1202
+ * Kind must appear as a dotted segment of `type` so names like
1203
+ * `host.presentation.cue.started` fail at definition time instead of
1204
+ * silently inferring a bogus kind.
1205
+ */
1206
+
1207
+ type ContractFieldType = 'string' | 'number' | 'boolean' | 'object' | 'array';
1208
+ type PayloadFieldSpec = {
1209
+ type: ContractFieldType;
1210
+ optional?: boolean;
1211
+ };
1212
+ type PayloadSchema = {
1213
+ /** Payload schema version. Bump on any field change. */
1214
+ version: number;
1215
+ fields: Record<string, PayloadFieldSpec>;
1216
+ };
1217
+ type ContractDiagnostic = {
1218
+ code: string;
1219
+ detail: string;
1220
+ path?: string;
1221
+ };
1222
+ type EventContractManifest = {
1223
+ type: string;
1224
+ kind: EventKind;
1225
+ version: number;
1226
+ fields: Record<string, {
1227
+ type: ContractFieldType;
1228
+ optional: boolean;
1229
+ }>;
1230
+ emitPattern: string;
1231
+ subscribePattern: string;
1232
+ };
1233
+ type PayloadValidation = {
1234
+ ok: true;
1235
+ payload: unknown;
1236
+ } | {
1237
+ ok: false;
1238
+ errors: ContractDiagnostic[];
1239
+ };
1240
+ type EventContract = {
1241
+ type: string;
1242
+ kind: EventKind;
1243
+ payloadSchema: PayloadSchema;
1244
+ emitPattern: string;
1245
+ subscribePattern: string;
1246
+ toManifest(): EventContractManifest;
1247
+ validatePayload(payload: unknown): PayloadValidation;
1248
+ };
1249
+ type ContractRegistry = {
1250
+ get(type: string): EventContract | undefined;
1251
+ manifest(): EventContractManifest[];
1252
+ validateEnvelope(event: EventEnvelope): PayloadValidation;
1253
+ asRouterValidate(event: EventEnvelope): ValidateResult;
1254
+ };
1255
+
1256
+ declare const REPLAY_INSPECTOR_SCHEMA_VERSION: 1;
1257
+ type ReplayTraceFilter = {
1258
+ types?: string[];
1259
+ sources?: string[];
1260
+ outcomes?: RouterDecisionOutcome[];
1261
+ correlationId?: string;
1262
+ };
1263
+ type CreateReplayInspectorOptions = {
1264
+ redactedKeys?: string[];
1265
+ maxRecords?: number;
1266
+ /** Optional contracts so records can include payload schema version. */
1267
+ registry?: Pick<ContractRegistry, 'get'>;
1268
+ };
1269
+ type InspectorRecord = {
1270
+ index: number;
1271
+ turn: number;
1272
+ time: number;
1273
+ outcome: RouterDecisionOutcome;
1274
+ reason?: RouterDecisionReason;
1275
+ detail?: string;
1276
+ source: string;
1277
+ target?: string;
1278
+ type: string;
1279
+ kind?: EventKind;
1280
+ envelopeId?: string;
1281
+ priorEnvelopeId?: string;
1282
+ correlationId?: string;
1283
+ causationId?: string;
1284
+ hops?: number;
1285
+ seq?: number;
1286
+ idempotencyKey?: string;
1287
+ schemaVersion?: number;
1288
+ payloadSchemaVersion?: number;
1289
+ deliveredTo: string[];
1290
+ payload?: unknown;
1291
+ };
1292
+ type CausationTreeNode = {
1293
+ envelopeId?: string;
1294
+ type: string;
1295
+ source: string;
1296
+ outcome: RouterDecisionOutcome;
1297
+ reason?: RouterDecisionReason;
1298
+ children: CausationTreeNode[];
1299
+ };
1300
+ type ReplayParticipantSummary = {
1301
+ id: string;
1302
+ kind?: RuntimeGroupKind;
1303
+ emit: string[];
1304
+ subscribe: string[];
1305
+ authoritative: boolean;
1306
+ seed?: string;
1307
+ clock?: ClockSnapshot;
1308
+ state?: unknown;
1309
+ errorCount: number;
1310
+ lastError?: string;
1311
+ };
1312
+ type ReplayInspectorReport = {
1313
+ schemaVersion: typeof REPLAY_INSPECTOR_SCHEMA_VERSION;
1314
+ participants: ReplayParticipantSummary[];
1315
+ records: InspectorRecord[];
1316
+ trees: CausationTreeNode[];
1317
+ dropped: number;
1318
+ };
1319
+ type ReplayTapeAction = {
1320
+ kind: 'publish';
1321
+ event: EventInput;
1322
+ extras?: PublishExtras;
1323
+ } | {
1324
+ kind: 'dispatch';
1325
+ participantId: string;
1326
+ event: HostEvent;
1327
+ } | {
1328
+ kind: 'step';
1329
+ frames: number;
1330
+ } | {
1331
+ kind: 'asset';
1332
+ participantId: string;
1333
+ event: HostEvent;
1334
+ };
1335
+ type ReplayInspectorExport = {
1336
+ schemaVersion: typeof REPLAY_INSPECTOR_SCHEMA_VERSION;
1337
+ origin: number;
1338
+ redactedKeys: string[];
1339
+ participants: ReplayParticipantSummary[];
1340
+ tape: ReplayTapeAction[];
1341
+ records: InspectorRecord[];
1342
+ snapshots: Record<string, unknown>;
1343
+ };
1344
+ type ReplayCompareResult = {
1345
+ ok: true;
1346
+ } | {
1347
+ ok: false;
1348
+ detail: string;
1349
+ };
1350
+ type BoundReplaySession = {
1351
+ publish(event: EventInput, extras?: PublishExtras): EventEnvelope | undefined;
1352
+ dispatch(participantId: string, event: HostEvent): void;
1353
+ step(frames?: number): Promise<void>;
1354
+ report(): Promise<ReplayInspectorReport>;
1355
+ exportTrace(filter?: ReplayTraceFilter): Promise<ReplayInspectorExport>;
1356
+ unbind(): void;
1357
+ };
1358
+ type ReplayInspector = {
1359
+ watchRouter(router: EventRouter): () => void;
1360
+ bind(group: RuntimeGroup): BoundReplaySession;
1361
+ importTrace(exported: ReplayInspectorExport | string): void;
1362
+ exportTrace(filter?: ReplayTraceFilter): ReplayInspectorExport;
1363
+ report(filter?: ReplayTraceFilter): ReplayInspectorReport;
1364
+ causationTree(correlationId?: string): CausationTreeNode[];
1365
+ records(): InspectorRecord[];
1366
+ reset(): void;
1367
+ destroy(): void;
1368
+ };
1369
+ declare function compareReplayTraces(expected: InspectorRecord[], actual: InspectorRecord[]): ReplayCompareResult;
1370
+ declare function createReplayInspector(options?: CreateReplayInspectorOptions): ReplayInspector;
1371
+ declare function replayExportedTrace(exported: ReplayInspectorExport, group: RuntimeGroup, options?: CreateReplayInspectorOptions): Promise<{
1372
+ inspector: ReplayInspector;
1373
+ report: ReplayInspectorReport;
1374
+ }>;
1375
+
1376
+ type CueEasing = 'linear' | 'ease-out';
1377
+ type CueDuplicatePolicy = 'ignore' | 'replace' | 'reject';
1378
+ type CueRepeatPolicy = {
1379
+ count: number;
1380
+ } | {
1381
+ forever: true;
1382
+ };
1383
+ type CueReducedMotionPolicy = 'skip' | 'complete' | {
1384
+ durationFrames: number;
1385
+ };
1386
+ type CueSpec = {
1387
+ name: string;
1388
+ idempotencyKey: string;
1389
+ /** Frame when the cue is eligible to start (before delay). Default: play frame. */
1390
+ startFrame?: number;
1391
+ durationFrames: number;
1392
+ delayFrames?: number;
1393
+ easing?: CueEasing;
1394
+ repeat?: CueRepeatPolicy;
1395
+ /**
1396
+ * When the timeline is in reduced-motion mode: skip (complete immediately),
1397
+ * complete (same), or a shorter duration. Default `complete`.
1398
+ */
1399
+ reducedMotion?: CueReducedMotionPolicy;
1400
+ onDuplicate?: CueDuplicatePolicy;
1401
+ };
1402
+ type CuePhase = 'scheduled' | 'active' | 'completed' | 'cancelled';
1403
+ type CueView = {
1404
+ name: string;
1405
+ idempotencyKey: string;
1406
+ phase: CuePhase;
1407
+ startFrame: number;
1408
+ durationFrames: number;
1409
+ delayFrames: number;
1410
+ easing: CueEasing;
1411
+ progress: number;
1412
+ repeatIndex: number;
1413
+ };
1414
+
1415
+ /**
1416
+ * Copyright (c) 2026 Aaron Boyarsky
1417
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
1418
+ * See packages/engine/LICENSE
1419
+ *
1420
+ * Deterministic audio-cue timeline. Scheduling is frame-stepped via
1421
+ * `createPresentationTimeline`; PCM output is not part of the event trace.
1422
+ */
1423
+
1424
+ declare const AUDIO_CUE_EVENTS: readonly ["audio.cue.scheduled", "audio.cue.started", "audio.cue.skipped", "audio.cue.failed"];
1425
+ type AudioCueEventType = (typeof AUDIO_CUE_EVENTS)[number];
1426
+ type AudioCueSkipReason = 'reduced-sensory' | 'muted' | 'unauthorized';
1427
+ type AudioCueFailReason = 'asset-failed' | 'torn-down' | 'invalid';
1428
+ type AudioCueReason = AudioCueSkipReason | AudioCueFailReason;
1429
+ type AudioCueSpec = CueSpec & {
1430
+ assetId: string;
1431
+ channelId?: string;
1432
+ participantId?: string;
1433
+ priority?: number;
1434
+ gain?: number;
1435
+ };
1436
+ type AudioCueView = CueView & {
1437
+ assetId: string;
1438
+ channelId: string;
1439
+ participantId?: string;
1440
+ priority: number;
1441
+ audioPhase: 'scheduled' | 'started' | 'skipped' | 'failed' | 'completed';
1442
+ };
1443
+ type AudioCueEvent = {
1444
+ type: AudioCueEventType;
1445
+ atFrame: number;
1446
+ name: string;
1447
+ idempotencyKey: string;
1448
+ assetId: string;
1449
+ channelId: string;
1450
+ participantId?: string;
1451
+ reason?: AudioCueReason;
1452
+ progress: number;
1453
+ };
1454
+ type AudioCueTimelineSnapshot = {
1455
+ frame: number;
1456
+ reducedSensory: boolean;
1457
+ cues: AudioCueView[];
1458
+ events: AudioCueEvent[];
1459
+ };
1460
+ type PlayAudioCueResult = {
1461
+ ok: true;
1462
+ cue: AudioCueView;
1463
+ } | {
1464
+ ok: false;
1465
+ reason: 'duplicate' | 'invalid';
1466
+ detail: string;
1467
+ };
1468
+ type AudioCueTimeline = {
1469
+ play(spec: AudioCueSpec): PlayAudioCueResult;
1470
+ step(frames?: number): AudioCueEvent[];
1471
+ cancel(idempotencyKey: string): boolean;
1472
+ reset(): void;
1473
+ snapshot(): AudioCueTimelineSnapshot;
1474
+ get(idempotencyKey: string): AudioCueView | undefined;
1475
+ dispose(): void;
1476
+ readonly frame: number;
1477
+ readonly reducedSensory: boolean;
1478
+ };
1479
+ type HeadlessAudioAdapter = {
1480
+ readonly broker: AudioBroker;
1481
+ readonly timeline: AudioCueTimeline;
1482
+ unlock(): Promise<AudioUnlockStatus>;
1483
+ play(spec: AudioCueSpec): PlayAudioCueResult;
1484
+ step(frames?: number): AudioCueEvent[];
1485
+ handleHostEvent(event: HostEvent): void;
1486
+ snapshot(): AudioCueTimelineSnapshot & {
1487
+ unlock: AudioUnlockStatus;
1488
+ muted: boolean;
1489
+ };
1490
+ destroy(): void;
1491
+ };
1492
+ declare function createHeadlessAudioAdapter(options?: {
1493
+ reducedSensory?: boolean;
1494
+ originFrame?: number;
1495
+ }): HeadlessAudioAdapter;
1496
+
1497
+ /**
1498
+ * Copyright (c) 2026 Aaron Boyarsky
1499
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
1500
+ * See packages/engine/LICENSE
1501
+ *
1502
+ * Node/jsdom test helpers. Import from `@cyberart-io/engine/headless`.
1503
+ * Production carts and browser hosts must import `@cyberart-io/engine` instead
1504
+ * so Vite never walks `node:fs/promises`.
1505
+ */
1506
+
1507
+ type WriteComposedFrameResult = ComposedFrame & {
1508
+ path?: string;
1509
+ };
1510
+ /**
1511
+ * Headless composition-order capture. `captureComposedFrame()` stays on the
1512
+ * compositor (browser-safe). This helper writes PNG bytes when a path is given.
1513
+ */
1514
+ declare function writeComposedFrame(compositor: Compositor, path?: string): Promise<WriteComposedFrameResult>;
1515
+
1516
+ export { type BoundReplaySession, type CausationTreeNode, type CreateHeadlessHarnessOptions, type CreateHeadlessMultiCartHarnessOptions, type CreateReplayInspectorOptions, DEFAULT_HEADLESS_HEIGHT, DEFAULT_HEADLESS_WIDTH, type GlyphAtlas, HEADLESS_PNG_DATA_URL, type HeadlessAudioAdapter, type HeadlessCanvas2DSettings, type HeadlessFrameError, type HeadlessHarness, type HeadlessImageFixture, type HeadlessInspect, type HeadlessMultiCartHarness, HeadlessUnsupportedOperationError, type InspectorRecord, type InstallHeadlessCanvasOptions, type ReplayCompareResult, type ReplayInspector, type ReplayInspectorExport, type ReplayInspectorReport, type ReplayParticipantSummary, type ReplayTapeAction, type ReplayTraceFilter, UPDATE_GOLDEN_ENV, type VisualArtifactPaths, type VisualArtifacts, type VisualCompareOptions, type VisualCompareResult, type WriteComposedFrameResult, assertPixelsEqual, assertPngDataUrlsEqual, attachHeadlessCanvas2D, compareImageData, compareReplayTraces, createDefaultGlyphAtlas, createHeadlessAudioAdapter, createHeadlessHarness, createHeadlessMultiCartHarness, createImageFixture, createReplayInspector, decodePng, encodePng, encodePngDataUrl, getHeadlessSurface, imageDataFromPngDataUrl, installHeadlessCanvas, makeImageData, replayExportedTrace, setDefaultGlyphAtlas, shouldUpdateGolden, writeComposedFrame, writeVisualArtifacts };