@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.
- package/README.md +102 -21
- package/dist/headless.d.ts +779 -68
- package/dist/headless.js +1 -1
- package/dist/index.d.ts +1144 -9
- package/dist/index.js +1 -1
- package/docs/asset-resolver.md +4 -4
- package/docs/audio.md +152 -0
- package/docs/browser-harness.md +126 -0
- package/docs/capability-manifest.md +18 -7
- package/docs/compositor.md +103 -0
- package/docs/deterministic-mode.md +1 -1
- package/docs/events.md +75 -13
- package/docs/executable-modules.md +112 -0
- package/docs/headless-harness.md +44 -14
- package/docs/midi.md +128 -0
- package/docs/presentation-adapter.md +8 -8
- package/docs/presentation-cue.md +2 -2
- package/docs/replay-inspector.md +88 -0
- package/docs/runtime-group.md +9 -7
- package/docs/snapshots.md +102 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -89,13 +89,20 @@ Full API for the event router, deterministic replay, and CI harness (so agents c
|
|
|
89
89
|
|
|
90
90
|
- [Events and router](docs/events.md) — mailbox, envelope, typed contracts, `createEventRouter`, hops, idempotency, rejections
|
|
91
91
|
- [Deterministic mode](docs/deterministic-mode.md) — `step` / `schedule`, clocks, `ScriptedAction`, replay diffs
|
|
92
|
-
- [Headless harness](docs/headless-harness.md) — `createHeadlessHarness` from `@cyberart-io/engine/headless`,
|
|
92
|
+
- [Headless harness](docs/headless-harness.md) — `createHeadlessHarness` from `@cyberart-io/engine/headless`, software Canvas2D, `compareImageData` / `assertPixelsEqual`, inspect / screenshot
|
|
93
93
|
- [Presentation adapter](docs/presentation-adapter.md) — host-owned render model, intents, loading / error / unsupported
|
|
94
94
|
- [Asset resolver](docs/asset-resolver.md) — host-pluggable images/audio/fonts/spritesheets, cache, preload, fallbacks
|
|
95
95
|
- [Presentation cue](docs/presentation-cue.md) — deterministic `createPresentationTimeline`, duplicate policy, reduced-motion, lifecycle events
|
|
96
96
|
- [Capability manifest](docs/capability-manifest.md) — versioned JSON for runtime features, phases, managers, assets, events, permissions, integrations
|
|
97
|
+
- [Executable modules](docs/executable-modules.md) — trusted versioned factories, host allowlists, isolation, per-module failures
|
|
97
98
|
- [Normalized geometry](docs/normalized-geometry.md) — coordinate spaces, contain/cover/crop layout, landmarks, hit regions, debug overlay
|
|
98
99
|
- [Runtime group](docs/runtime-group.md) — `createRuntimeGroup`, shared router attach, lockstep clock; `createHeadlessMultiCartHarness` from `@cyberart-io/engine/headless`
|
|
100
|
+
- [Compositor](docs/compositor.md) — `createCompositor`, transparent stacking, `screen` blend, `writeComposedFrame`
|
|
101
|
+
- [Browser harness](docs/browser-harness.md) — `createBrowserHarness`, DOM clicks, viewport/DPR, composed screenshots
|
|
102
|
+
- [Replay inspector](docs/replay-inspector.md) — `createReplayInspector`, causation trees, redacted export/import, headless replay
|
|
103
|
+
- [MIDI](docs/midi.md) — host-owned `MidiManager`, note/CC/pitch in and out, `inject` / fake port, structured `requestAccess`
|
|
104
|
+
- [Snapshots](docs/snapshots.md) — versioned envelope, `defineSnapshot` / `applySnapshotMigrations`, host-owned `hostState`
|
|
105
|
+
- [Audio](docs/audio.md) — `createAudioBroker`, `createAudioCueTimeline`, headless cue traces (PCM is not deterministic)
|
|
99
106
|
|
|
100
107
|
## Write a cart
|
|
101
108
|
|
|
@@ -150,6 +157,8 @@ const cart = runtime.mount(artProject, {
|
|
|
150
157
|
| `audio` | none | Libraries to unlock if `unlockAudio()` runs before `mount`. After mount, the cart’s `metadata.audio` wins. |
|
|
151
158
|
| `deterministic` | off | Host-controlled clock, `step`/`advance`, and scripted input/assets. Leave unset for live kaleidoscope / Art Blocks. |
|
|
152
159
|
| `assets` | off | Host `AssetResolver` plus engine cache/preload. Carts keep logical refs. Leave unset when the piece has no media. |
|
|
160
|
+
| `audioBroker` | off | Shared `createAudioBroker` instance. Optional. Omit when the cart only uses `metadata.audio: 'tone'`. |
|
|
161
|
+
| `audioParticipantId` | none | Group participant id to authorize / teardown on this runtime. |
|
|
153
162
|
|
|
154
163
|
`CartHandle` (what `mount` returns):
|
|
155
164
|
|
|
@@ -162,8 +171,10 @@ const cart = runtime.mount(artProject, {
|
|
|
162
171
|
| `step(frames)` / `advance(ms)` | Deterministic ticks only. Throw if `deterministic` was not set. |
|
|
163
172
|
| `schedule(action)` | Queue a pointer/key/host-event/asset for a future frame. |
|
|
164
173
|
| `getClock()` / `getRandomState()` / `getReplayMetadata()` | Replay inspection. |
|
|
165
|
-
| `exportState()` / `exportStateJSON()` | Pause-safe serializable
|
|
166
|
-
| `importState(bundle)` | Restore a bundle
|
|
174
|
+
| `exportState()` / `exportStateJSON()` | Pause-safe serializable `CartStateBundle` (schema 1 blob). |
|
|
175
|
+
| `importState(bundle)` | Restore a bundle, JSON string, or snapshot envelope (migrates then loads `engineState`). |
|
|
176
|
+
| `exportSnapshot()` / `exportSnapshotJSON()` | Schema 2 envelope wrapping the bundle plus clock, rng, optional `hostState`. |
|
|
177
|
+
| `importSnapshot(envelope)` | Migrate to the current schema and restore `engineState`. Hosts reapply `hostState`. |
|
|
167
178
|
| `destroy()` | Unload this cart. Idempotent. |
|
|
168
179
|
| `needsAudio` | True when the cart declared audio libraries. Use this to show a click overlay. |
|
|
169
180
|
| `paused` / `isPrepared` / `isLoopRunning` | Loop flags. |
|
|
@@ -196,6 +207,14 @@ Install the peer: `npm i tone@^14.8.15`. Call `cart.start()` **inside a click or
|
|
|
196
207
|
|
|
197
208
|
If a click can happen before `mount` finishes, pass the same `audio` into `createRuntime` and/or call `runtime.unlockAudio()` from that click. After mount, the cart’s metadata wins.
|
|
198
209
|
|
|
210
|
+
Several carts on one page should share **one** `createAudioBroker` (pass it to `createRuntime` / `createRuntimeGroup`). One `unlock()` covers authorized participants. Channel gain, mute, duck, and priority live on the broker; cue timing is `createAudioCueTimeline` / `scheduleAudioCue` (presentation-frame clock). Headless CI records cue events with `createHeadlessAudioAdapter` from `@cyberart-io/engine/headless` — the event trace is deterministic, PCM is not. Full API: [audio](docs/audio.md).
|
|
211
|
+
|
|
212
|
+
## MIDI
|
|
213
|
+
|
|
214
|
+
Host-owned `MidiManager` — carts and hosts construct it. The runtime does not pass it into `getDefaultState`. Subscribe to note / CC / pitch, `inject` for tests without hardware, and `send` note-on/off, CC, or raw bytes through an injectable port. `requestAccess()` wraps `navigator.requestMIDIAccess` when present; a missing API or denied permission is a structured result, so silent carts keep running. Headless/jsdom has no Web MIDI; inject and send still work via a test port.
|
|
215
|
+
|
|
216
|
+
Full API and the reproduce command: [MIDI](docs/midi.md). Capability manifests already list `midi` as an integration.
|
|
217
|
+
|
|
199
218
|
## Seed
|
|
200
219
|
|
|
201
220
|
Pass `seed` when you want a deterministic output (for example from `?hash=`):
|
|
@@ -244,9 +263,9 @@ CI and agents should drive the **same** `createRuntime({ deterministic })` path.
|
|
|
244
263
|
- Carts and production hosts: `import { createRuntime } from '@cyberart-io/engine'`
|
|
245
264
|
- Tests, CI, and frame capture: `import { createHeadlessHarness, createHeadlessMultiCartHarness } from '@cyberart-io/engine/headless'`
|
|
246
265
|
|
|
247
|
-
`@cyberart-io/engine` does not re-export the harness. `installHeadlessCanvas` is the documented jsdom install (test-only). `createHeadlessHarness` sizes a container, mounts, and wraps step / input / inspect / snapshot.
|
|
266
|
+
`@cyberart-io/engine` does not re-export the harness. `installHeadlessCanvas` is the documented jsdom install (test-only). It attaches a deterministic software Canvas2D (paths, arcs, atlas text, transforms, clip, alpha, gradients, `drawImage`). Unsupported ops throw `HeadlessUnsupportedOperationError`. `createHeadlessHarness` sizes a container, mounts, and wraps step / input / inspect / snapshot. Pixel compare: `compareImageData`, `assertPixelsEqual`, `writeVisualArtifacts` (`CYBERART_UPDATE_GOLDEN=1` to refresh goldens).
|
|
248
267
|
|
|
249
|
-
Full options, `click` clock rule, Node-only `captureFrame`, remount, and the reproduce command: [headless harness](docs/headless-harness.md).
|
|
268
|
+
Full options, `click` clock rule, Node-only `captureFrame`, remount, visual asserts, and the reproduce command: [headless harness](docs/headless-harness.md).
|
|
250
269
|
|
|
251
270
|
```ts
|
|
252
271
|
import { createHeadlessHarness } from '@cyberart-io/engine/headless';
|
|
@@ -285,11 +304,11 @@ import {
|
|
|
285
304
|
createEventRouter,
|
|
286
305
|
} from '@cyberart-io/engine';
|
|
287
306
|
|
|
288
|
-
const exit = defineIntent('
|
|
307
|
+
const exit = defineIntent('host.intent.exit-requested', {
|
|
289
308
|
version: 1,
|
|
290
309
|
fields: { exitId: { type: 'string' } },
|
|
291
310
|
});
|
|
292
|
-
const room = defineStateEvent('
|
|
311
|
+
const room = defineStateEvent('host.state.room-changed', {
|
|
293
312
|
version: 1,
|
|
294
313
|
fields: { roomId: { type: 'string' } },
|
|
295
314
|
});
|
|
@@ -302,6 +321,26 @@ const router = createEventRouter({
|
|
|
302
321
|
router.attach('presentation', runtime.hostChannel, deriveAttachOptions(contracts, 'cart'));
|
|
303
322
|
```
|
|
304
323
|
|
|
324
|
+
## Presentation cue
|
|
325
|
+
|
|
326
|
+
Frame-stepped effects (overlays, pulses, scene fades). No `setTimeout` / rAF. Drive `step` from the same clock as deterministic `cart.step`.
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
import { createPresentationTimeline } from '@cyberart-io/engine';
|
|
330
|
+
|
|
331
|
+
const timeline = createPresentationTimeline({ originFrame: 0, reducedMotion: false });
|
|
332
|
+
timeline.play({
|
|
333
|
+
name: 'checkmark',
|
|
334
|
+
idempotencyKey: 'gold',
|
|
335
|
+
durationFrames: 90,
|
|
336
|
+
easing: 'ease-out',
|
|
337
|
+
onDuplicate: 'replace',
|
|
338
|
+
});
|
|
339
|
+
timeline.step(90);
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Lifecycle names `cue.started` / `cue.completed` / `cue.cancelled` / `cue.replaced` are local to the timeline. They are not router envelopes unless you define a contract. Duplicate policy, late-play catch-up, and reduced-motion: [presentation cue](docs/presentation-cue.md).
|
|
343
|
+
|
|
305
344
|
## Capability manifest
|
|
306
345
|
|
|
307
346
|
JSON for what a cart needs from the host. `validateCapabilityManifest` returns structured diagnostics instead of throwing.
|
|
@@ -313,14 +352,14 @@ import {
|
|
|
313
352
|
} from '@cyberart-io/engine';
|
|
314
353
|
|
|
315
354
|
const defined = defineCapabilityManifest({
|
|
316
|
-
id: '
|
|
355
|
+
id: 'host.presentation',
|
|
317
356
|
runtime: { minContractVersion: 1, features: ['router'] },
|
|
318
357
|
phases: ['loading', 'ready', 'error', 'unsupported'],
|
|
319
358
|
managers: ['pointer', 'hostChannel'],
|
|
320
|
-
assets: { kinds: ['image'], declarations: [{ id: '
|
|
321
|
-
acceptedEvents: ['
|
|
322
|
-
emittedEvents: ['
|
|
323
|
-
permissions: { emit: ['
|
|
359
|
+
assets: { kinds: ['image'], declarations: [{ id: 'backdrop', kind: 'image' }] },
|
|
360
|
+
acceptedEvents: ['host.state.*'],
|
|
361
|
+
emittedEvents: ['host.intent.exit-requested'],
|
|
362
|
+
permissions: { emit: ['host.intent.*'], subscribe: ['host.state.*'] },
|
|
324
363
|
integrations: ['tone'],
|
|
325
364
|
});
|
|
326
365
|
if (!defined.ok) throw new Error(defined.errors.map((e) => e.detail).join('; '));
|
|
@@ -328,13 +367,47 @@ validateCapabilityManifest(defined.manifest, {
|
|
|
328
367
|
contractVersion: 1,
|
|
329
368
|
features: ['router'],
|
|
330
369
|
integrations: ['tone'],
|
|
331
|
-
emit: ['
|
|
332
|
-
subscribe: ['
|
|
370
|
+
emit: ['host.intent.*'],
|
|
371
|
+
subscribe: ['host.state.*'],
|
|
333
372
|
});
|
|
334
373
|
```
|
|
335
374
|
|
|
336
375
|
Fields, host allowlists, and diagnostic codes: [capability manifest](docs/capability-manifest.md).
|
|
337
376
|
|
|
377
|
+
## Executable modules
|
|
378
|
+
|
|
379
|
+
Trusted factories keyed by exact `id` + `version`. The host allowlists which refs may load. There is no `eval` / `new Function` path for cart-supplied strings.
|
|
380
|
+
|
|
381
|
+
```ts
|
|
382
|
+
import { createExecutableModuleHost } from '@cyberart-io/engine';
|
|
383
|
+
|
|
384
|
+
const host = createExecutableModuleHost({
|
|
385
|
+
allowlist: [
|
|
386
|
+
{ id: 'overlay-fx', version: '1.0.0' },
|
|
387
|
+
{ id: 'host.module', version: '1.0.0' },
|
|
388
|
+
],
|
|
389
|
+
limits: { maxInvokeMs: 16, maxInvokesPerTurn: 4 },
|
|
390
|
+
modules: [
|
|
391
|
+
{
|
|
392
|
+
id: 'overlay-fx',
|
|
393
|
+
version: '1.0.0',
|
|
394
|
+
create: (capabilities) => ({
|
|
395
|
+
invoke: (input, { signal }) => {
|
|
396
|
+
if (signal.aborted) return;
|
|
397
|
+
return { input, seed: capabilities.seed };
|
|
398
|
+
},
|
|
399
|
+
}),
|
|
400
|
+
},
|
|
401
|
+
],
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const allowed = await host.invoke({ id: 'overlay-fx', version: '1.0.0' });
|
|
405
|
+
await host.invoke({ id: 'overlay-fx', version: '2.0.0' }); // version-mismatch, not invoked
|
|
406
|
+
host.destroy();
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
Unknown ids, wrong versions, and refs missing from the allowlist fail closed. A throwing module returns `{ ok: false, error }` and the next allowlisted invoke still runs. Isolation is in-process (trusted factories), not a worker or iframe. Carts may declare required refs as optional `modules.refs` on a [capability manifest](docs/capability-manifest.md). Full API and the reproduce command: [executable modules](docs/executable-modules.md).
|
|
410
|
+
|
|
338
411
|
## Runtime group
|
|
339
412
|
|
|
340
413
|
Several production carts, one router, one lockstep clock. Use this instead of intercepting each `onEvent` and republishing by hand.
|
|
@@ -345,17 +418,17 @@ import { createRuntimeGroup } from '@cyberart-io/engine';
|
|
|
345
418
|
const group = createRuntimeGroup({
|
|
346
419
|
origin: 0,
|
|
347
420
|
participants: [
|
|
348
|
-
{ id: 'effects', cart: effectsCart, emit: ['ambience.intent.*'], subscribe: ['
|
|
421
|
+
{ id: 'effects', cart: effectsCart, emit: ['ambience.intent.*'], subscribe: ['host.state.*'] },
|
|
349
422
|
{ id: 'ambience', cart: ambienceCart, subscribe: ['ambience.intent.*'] },
|
|
350
423
|
],
|
|
351
424
|
});
|
|
352
|
-
group.publish({ type: '
|
|
425
|
+
group.publish({ type: 'host.state.accepted', kind: 'state', payload: { id: 'north' } });
|
|
353
426
|
await group.step(2);
|
|
354
427
|
const { trace } = await group.inspect();
|
|
355
428
|
group.destroy();
|
|
356
429
|
```
|
|
357
430
|
|
|
358
|
-
Headless / CI: `createHeadlessMultiCartHarness` from `@cyberart-io/engine/headless` is the same handle after `installHeadlessCanvas()`. Full options: [runtime group](docs/runtime-group.md).
|
|
431
|
+
Headless / CI: `createHeadlessMultiCartHarness` from `@cyberart-io/engine/headless` is the same handle after `installHeadlessCanvas()`. Full options: [runtime group](docs/runtime-group.md). Causation trees, redaction, and tape replay: [replay inspector](docs/replay-inspector.md).
|
|
359
432
|
|
|
360
433
|
## Normalized geometry
|
|
361
434
|
|
|
@@ -400,7 +473,7 @@ const adapter = attachPresentationAdapter(harness);
|
|
|
400
473
|
adapter.present({
|
|
401
474
|
contractVersion: 1,
|
|
402
475
|
phase: 'ready',
|
|
403
|
-
view: { title: '
|
|
476
|
+
view: { title: 'Overlook', regions: [] },
|
|
404
477
|
});
|
|
405
478
|
```
|
|
406
479
|
|
|
@@ -418,7 +491,7 @@ import {
|
|
|
418
491
|
} from '@cyberart-io/engine';
|
|
419
492
|
|
|
420
493
|
const roomAssets = [
|
|
421
|
-
{ id: '
|
|
494
|
+
{ id: 'backdrop', ref: 'moltazine:post/porch-1#primary-image', type: 'image' as const },
|
|
422
495
|
{
|
|
423
496
|
id: 'ambience',
|
|
424
497
|
ref: 'world:asset/stream-loop',
|
|
@@ -453,14 +526,22 @@ The same declarations produce fixture URLs locally and CDN URLs (plus a typed CO
|
|
|
453
526
|
|
|
454
527
|
## Save and load
|
|
455
528
|
|
|
456
|
-
Live `state` is not JSON-safe (audio nodes, managers, typed arrays, possible cycles). `snapshot()` is PNG + seed. Use export/import when you want the simulation itself
|
|
529
|
+
Live `state` is not JSON-safe (audio nodes, managers, typed arrays, possible cycles). `snapshot()` is PNG + seed. Use export/import when you want the simulation itself.
|
|
530
|
+
|
|
531
|
+
`exportState` still returns the engine-owned `CartStateBundle` (`CART_STATE_BUNDLE_VERSION = 1`) so existing carts keep working. The versioned envelope (`SNAPSHOT_SCHEMA_VERSION = 2`) wraps that blob in `engineState` and keeps host-owned JSON in `hostState` (or a `hostStateRef`). Hosts persist the envelope; Cyberart is not the database. Full field list, migration registry, and diagnostic codes: [versioned snapshots](docs/snapshots.md).
|
|
457
532
|
|
|
458
533
|
```ts
|
|
459
534
|
const bundle = await cart.exportState();
|
|
460
535
|
const json = await cart.exportStateJSON();
|
|
536
|
+
const envelope = await cart.exportSnapshot({
|
|
537
|
+
cartVersion: '3',
|
|
538
|
+
hostState: { sceneId: 'alpha' },
|
|
539
|
+
});
|
|
461
540
|
|
|
462
541
|
await cart.importState(bundle);
|
|
463
542
|
await cart.importState(json);
|
|
543
|
+
await cart.importState(envelope);
|
|
544
|
+
await cart.importSnapshot(envelope);
|
|
464
545
|
```
|
|
465
546
|
|
|
466
547
|
Both pause, wait until any in-flight `update` finishes, then restore the previous pause flag. Import tears down, runs `getDefaultState` again (fresh managers, empty audio graph), overlays the save onto that scaffold unless `getDefaultState` already returned the revived `customState`, and restores `framesElapsed`. Works on a prepared cart before `start()`. Throws if nothing is prepared, or if the bundle is incompatible (`IncompatibleCartStateError`).
|
|
@@ -524,7 +605,7 @@ A canvas the host adopted is left in place on destroy; a canvas the engine creat
|
|
|
524
605
|
|
|
525
606
|
## Publishing this package (maintainers)
|
|
526
607
|
|
|
527
|
-
Not part of writing a cart. Engine source lives in `packages/engine/src/` (not mixed into the site). The npm tarball is built from `packages/engine/src/index.ts` and `packages/engine/src/headless.ts` and contains minified `dist/index.js` + `dist/headless.js`, rolled-up `.d.ts` for both, `LICENSE`, `README.md`, `docs/` (
|
|
608
|
+
Not part of writing a cart. Engine source lives in `packages/engine/src/` (not mixed into the site). The npm tarball is built from `packages/engine/src/index.ts` and `packages/engine/src/headless.ts` and contains minified `dist/index.js` + `dist/headless.js`, rolled-up `.d.ts` for both, `LICENSE`, `README.md`, `docs/` (including compositor, browser harness, replay inspector, MIDI, executable modules, snapshots, and audio), and `package.json`. Site code that still imports `src/ui/lib/...` hits thin re-export shims so those paths keep working.
|
|
528
609
|
|
|
529
610
|
```bash
|
|
530
611
|
pnpm run pack:engine
|