@cyberart-io/engine 0.0.7 → 0.0.9
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/CHANGELOG.md +37 -0
- package/README.md +308 -1
- package/dist/headless.d.ts +2584 -61
- package/dist/headless.js +1 -1
- package/dist/index.d.ts +2585 -244
- package/dist/index.js +1 -1
- package/docs/asset-resolver.md +1 -1
- package/docs/browser-harness.md +1 -1
- package/docs/capability-manifest.md +1 -1
- package/docs/content-revision.md +64 -0
- package/docs/events.md +1 -1
- package/docs/frame-benchmark.md +107 -0
- package/docs/headless-harness.md +6 -2
- package/docs/job-orchestration.md +55 -0
- package/docs/portal-lifecycle.md +50 -0
- package/docs/presentation-bindings.md +93 -0
- package/docs/presentation-sequences.md +150 -0
- package/docs/production-scenario.md +81 -0
- package/docs/remote-cart-manifest.md +66 -0
- package/docs/replay-inspector.md +1 -1
- package/docs/runtime-group.md +1 -1
- package/docs/selection-trace.md +60 -0
- package/docs/semantic-layers.md +72 -0
- package/docs/snapshots.md +1 -1
- package/docs/visual-layers.md +1 -1
- package/docs/world-graph.md +39 -0
- package/docs/world-patch.md +61 -0
- package/package.json +3 -2
package/docs/asset-resolver.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Host-owned URL policy. The engine caches, dedupes, tracks progress, applies timeouts and fallbacks, and (in live mode) dispatches `ASSET_READY_EVENT` / `ASSET_FAILED_EVENT`. Authored carts keep logical refs (`moltazine:post/<id>#primary-image`, `world:asset/…`, `library:…`, or ordinary URLs). the host, Cyberart, and a local preview each supply a different `AssetResolver`.
|
|
4
4
|
|
|
5
|
-
Deterministic timing: [deterministic mode](deterministic-mode.md). CI: [headless harness](headless-harness.md). Event mailbox: [events](events.md).
|
|
5
|
+
Deterministic timing: [deterministic mode](deterministic-mode.md). CI: [headless harness](headless-harness.md). Event mailbox: [events](events.md). Built on this: [remote cart manifests](remote-cart-manifest.md) (`createDeclaredAssetResolver`, declared URLs and hashes only), [content revision](content-revision.md) (resolves every declared asset before one atomic bundle swap), [selection trace](selection-trace.md) (records which logical ref, hash, and fallback won).
|
|
6
6
|
|
|
7
7
|
Back to the [package README](../README.md).
|
|
8
8
|
|
package/docs/browser-harness.md
CHANGED
|
@@ -4,7 +4,7 @@ Opt-in harness that mounts a caller-supplied host into a real viewport, optional
|
|
|
4
4
|
|
|
5
5
|
Import `createBrowserHarness` from **`@cyberart-io/engine`**. It does not load `node:fs`. In Vitest/jsdom, call `installHeadlessCanvas()` from **`@cyberart-io/engine/headless`** first so `Canvas2D` pixels exist. Production Player / kaleidoscope should not call this harness.
|
|
6
6
|
|
|
7
|
-
Related: [normalized geometry](normalized-geometry.md), [presentation adapter](presentation-adapter.md), [compositor](compositor.md), [headless harness](headless-harness.md).
|
|
7
|
+
Related: [normalized geometry](normalized-geometry.md), [presentation adapter](presentation-adapter.md), [compositor](compositor.md), [headless harness](headless-harness.md). Built on this: [production scenario](production-scenario.md) (mounts the full host/runtime/cart composition through this harness), [semantic layers](semantic-layers.md) (`semanticLayers` option, a11y snapshot).
|
|
8
8
|
|
|
9
9
|
Back to the [package README](../README.md).
|
|
10
10
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Capability manifest
|
|
2
2
|
|
|
3
|
-
Versioned JSON for what a cart needs from the host: runtime contract, lifecycle phases, managers, asset kinds, event patterns, permissions, and integrations. Related: [events](events.md) (emit / subscribe patterns), [asset resolver](asset-resolver.md) (kinds only — this file does not resolve URLs), [calculation carts](calculation-carts.md) (`kind`).
|
|
3
|
+
Versioned JSON for what a cart needs from the host: runtime contract, lifecycle phases, managers, asset kinds, event patterns, permissions, and integrations. Related: [events](events.md) (emit / subscribe patterns), [asset resolver](asset-resolver.md) (kinds only — this file does not resolve URLs), [calculation carts](calculation-carts.md) (`kind`). Built on this: [remote cart manifests](remote-cart-manifest.md) (signed catalog with optional nested `capabilities` + host `grants`), [content revision](content-revision.md) (required grants checked before activation).
|
|
4
4
|
|
|
5
5
|
Back to the [package README](../README.md).
|
|
6
6
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Content revision activation
|
|
2
|
+
|
|
3
|
+
Atomic staging and commit for an external content revision (room art, masks, portraits, line audio). Adapters load catalogs; the activator resolves every declared asset and swaps one complete bundle. Consumers see the last known good revision or the new revision — never a mix.
|
|
4
|
+
|
|
5
|
+
Related: [Asset resolver](asset-resolver.md) (`createDeclaredAssetResolver`), [Remote cart manifests](remote-cart-manifest.md) (`verifyRemoteCartSignature`, grants), [Capability manifest](capability-manifest.md), [Snapshots](snapshots.md). Back to the [package README](../README.md).
|
|
6
|
+
|
|
7
|
+
## One-command reproduce (this repo)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm exec vitest run packages/engine/src/canvas/cyb-79-content-revision.repro.spec.ts
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## When to use
|
|
14
|
+
|
|
15
|
+
| Surface | Import | Use |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| Production host | `createContentRevisionActivator` from `@cyberart-io/engine` | Persist `snapshot()` in envelope `hostState`. Read `activeBundle()` / `inspect()` for presentation. |
|
|
18
|
+
| Vitest / jsdom | same, or `@cyberart-io/engine/headless` | `createStaticContentAdapter` plus in-process `createRemoteContentAdapter({ fetch })`. |
|
|
19
|
+
|
|
20
|
+
Moltazine World is the first **adapter**, not a hard-coded SDK. Tests inject a fake remote fetch. There is no product UI here.
|
|
21
|
+
|
|
22
|
+
## Contract
|
|
23
|
+
|
|
24
|
+
1. **Identity.** A bundle is `contentId` + exact `revision` + `manifestVersion` + asset hashes + publisher/source + adapter id. Snapshots never store `"latest"`.
|
|
25
|
+
2. **Adapters.** `createStaticContentAdapter` (local catalog) and `createRemoteContentAdapter` (injected fetch, signed CYB-74 JSON, optional `{ world, revision, manifest }` envelope) implement the same load/activation contract. Static `discover` picks the numeric-aware newest revision id (`10` beats `9`).
|
|
26
|
+
3. **Staging.** Resolve every declared asset, validate schema, hashes, signatures, and required grants **before** `activeBundle()` changes.
|
|
27
|
+
4. **Commit.** One atomic swap, exactly one `content.revision.state.activated` event per accepted revision. Idempotent activate of the live bundle/revision id does not emit again.
|
|
28
|
+
5. **Visibility.** `inspect()` / `activeBundle()` return a complete revision or `null`. Staging identity may appear on `inspect().staging`; resources stay on the live bundle.
|
|
29
|
+
6. **Rollback.** Last-known-good is retained. Hash/schema/capability/health-check failure keeps or restores that bundle and emits a machine-readable rejection. `rollback()` is idempotent.
|
|
30
|
+
7. **Optional assets** may omit or use a declared hashed fallback. Required assets must all succeed.
|
|
31
|
+
8. **Supersede.** A newer `activate` aborts in-flight staging, emits `superseded`, then stages the new revision. A stale health check after a newer commit returns `stale-health-check`, not `superseded`.
|
|
32
|
+
9. **Pin.** `pin()` freezes live movement after a bundle is active: `activate` of another revision, `rollback`, and `restore` that would change the live bundle all fail with `pinned`. Discover still works. Idempotent activate/rollback of the current bundle is allowed.
|
|
33
|
+
10. **Restore.** Validate the entire snapshot (schema version, exact revisions known to adapters) before mutate. Unknown version or unknown revision leaves the live bundle unchanged.
|
|
34
|
+
11. **Cache.** Byte cache is keyed by digest and re-hashed on reuse.
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import {
|
|
38
|
+
createContentRevisionActivator,
|
|
39
|
+
createRemoteContentAdapter,
|
|
40
|
+
createStaticContentAdapter,
|
|
41
|
+
} from '@cyberart-io/engine';
|
|
42
|
+
|
|
43
|
+
const activator = createContentRevisionActivator({
|
|
44
|
+
adapters: [staticAdapter, remoteAdapter],
|
|
45
|
+
grants: createHostGrantSet(['audio']),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
await activator.activate({ contentId: 'room-overlook', revision: '8' });
|
|
49
|
+
activator.activeBundle(); // complete revision 8, or null
|
|
50
|
+
activator.snapshot(); // exact revision + hashes for hostState
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
| Event | When |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `content.revision.state.discovered` | Adapter reported an exact revision |
|
|
56
|
+
| `content.revision.state.staging` | Resolve/validate started; presentation unchanged |
|
|
57
|
+
| `content.revision.state.validated` | Declared assets, hashes, and capabilities passed |
|
|
58
|
+
| `content.revision.state.rejected` | Staging or validation failed; live bundle unchanged |
|
|
59
|
+
| `content.revision.state.activated` | Atomic commit of one complete bundle |
|
|
60
|
+
| `content.revision.state.rolled-back` | Health-check/activation failure or host `rollback()` |
|
|
61
|
+
| `content.revision.state.superseded` | Newer revision cancelled this staging attempt |
|
|
62
|
+
| `content.revision.diagnostic.lifecycle` | Machine-readable rejection detail |
|
|
63
|
+
|
|
64
|
+
Contracts: `contentRevisionEventContracts()`.
|
package/docs/events.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Events and router
|
|
2
2
|
|
|
3
|
-
Mailbox (one runtime) and `createEventRouter` (many carts). Prefer `createRuntimeGroup` when several carts should share one router and lockstep clock. Carts never receive the router object or another cart’s `HostChannel`. Related: [deterministic mode](deterministic-mode.md) (`now` / `createId` / `turn` per `step`), [presentation adapter](presentation-adapter.md) (`presentation.state.model`), [presentation cue](presentation-cue.md) (frame-local lifecycle, not routed unless you define a contract), [runtime group](runtime-group.md), [capability manifest](capability-manifest.md), [replay inspector](replay-inspector.md).
|
|
3
|
+
Mailbox (one runtime) and `createEventRouter` (many carts). Prefer `createRuntimeGroup` when several carts should share one router and lockstep clock. Carts never receive the router object or another cart’s `HostChannel`. Related: [deterministic mode](deterministic-mode.md) (`now` / `createId` / `turn` per `step`), [presentation adapter](presentation-adapter.md) (`presentation.state.model`), [presentation cue](presentation-cue.md) (frame-local lifecycle, not routed unless you define a contract), [runtime group](runtime-group.md), [capability manifest](capability-manifest.md), [replay inspector](replay-inspector.md). Modules that publish typed `*.state.*` / `*.diagnostic.*` contracts through this router: [content revision](content-revision.md), [job orchestration](job-orchestration.md), [world graph](world-graph.md), [world patch](world-patch.md), [production scenario](production-scenario.md), [presentation bindings](presentation-bindings.md), [presentation sequences](presentation-sequences.md).
|
|
4
4
|
|
|
5
5
|
Back to the [package README](../README.md).
|
|
6
6
|
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Frame performance benchmarking
|
|
2
|
+
|
|
3
|
+
Cart-agnostic tools for measuring update vs render cost. Use these for hash / trait investigations instead of one-off vitest probes.
|
|
4
|
+
|
|
5
|
+
Production kaleidoscope / Art Blocks playback **leave profiling unset**. When `onFrameTiming` is unset, the draw loop only does a single null check — no `performance.now()` calls.
|
|
6
|
+
|
|
7
|
+
Back to the [package README](../README.md). Related: [headless harness](headless-harness.md), [deterministic mode](deterministic-mode.md).
|
|
8
|
+
|
|
9
|
+
## Two surfaces
|
|
10
|
+
|
|
11
|
+
| Surface | Import | When to use |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| Headless bench | `@cyberart-io/engine/headless` → `benchmarkCartFrames` | CI / agent hash A/B without mounting a live runtime |
|
|
14
|
+
| Live hook | `@cyberart-io/engine` → `runtime.onFrameTiming` | Overlay / collect samples on the real `rAF` or `step` path |
|
|
15
|
+
|
|
16
|
+
## Headless: `benchmarkCartFrames`
|
|
17
|
+
|
|
18
|
+
Runs a cart's `update` / `render` loop with a virtual frame clock. Does not start audio or construct `AnimationManager`.
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import {
|
|
22
|
+
benchmarkCartFrames,
|
|
23
|
+
formatFrameBenchmarkResult,
|
|
24
|
+
} from '@cyberart-io/engine/headless';
|
|
25
|
+
|
|
26
|
+
const result = benchmarkCartFrames(artProject, hash, rawParams, {
|
|
27
|
+
width: 1920,
|
|
28
|
+
height: 1080,
|
|
29
|
+
frames: 30,
|
|
30
|
+
warmupFrames: 5,
|
|
31
|
+
prepareState: (state) => {
|
|
32
|
+
// e.g. skip Tone init in jsdom
|
|
33
|
+
(state as { audioContextStarted?: boolean }).audioContextStarted = true;
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
console.table(formatFrameBenchmarkResult(result));
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Result fields
|
|
41
|
+
|
|
42
|
+
| Field | Meaning |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `updateAvgMs` / `renderAvgMs` | Mean phase time over measured frames |
|
|
45
|
+
| `updateMaxMs` / `renderMaxMs` | Worst frame in the sample (catches sim hitches) |
|
|
46
|
+
| `totalAvgMs` / `estFps` | Combined average and `1000 / totalAvgMs` |
|
|
47
|
+
| `updatePct` / `renderPct` | Share of measured time |
|
|
48
|
+
|
|
49
|
+
`formatFrameBenchmarkResult` rounds for stable console / snapshot logs.
|
|
50
|
+
|
|
51
|
+
### Options
|
|
52
|
+
|
|
53
|
+
- `frames` (default 30), `warmupFrames` (default 5), `frameStepMs` (default `1000/60`)
|
|
54
|
+
- `width` / `height` (default 1920×1080)
|
|
55
|
+
- `tokenId` for `Random`
|
|
56
|
+
- `prepareState(state, featureState)` after `getDefaultState`
|
|
57
|
+
- `drawingContext` stub (default no-op `putImageData`)
|
|
58
|
+
|
|
59
|
+
One-command reproduce in this repo:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pnpm exec vitest run packages/engine/src/canvas/frameBenchmark.spec.ts
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Live: `onFrameTiming`
|
|
66
|
+
|
|
67
|
+
Optional per-frame sample on the production draw / `step` path.
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { createRuntime, type FrameTimingSample } from '@cyberart-io/engine';
|
|
71
|
+
|
|
72
|
+
const samples: FrameTimingSample[] = [];
|
|
73
|
+
|
|
74
|
+
const runtime = createRuntime({
|
|
75
|
+
container,
|
|
76
|
+
// optional at construction:
|
|
77
|
+
onFrameTiming: (sample) => samples.push(sample),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// or assign later / clear to restore zero overhead:
|
|
81
|
+
runtime.onFrameTiming = (sample) => {
|
|
82
|
+
console.log(sample.frame, sample.updateMs, sample.renderMs, sample.drawMs);
|
|
83
|
+
};
|
|
84
|
+
runtime.onFrameTiming = undefined;
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Sample shape
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
type FrameTimingSample = {
|
|
91
|
+
frame: number;
|
|
92
|
+
updateMs: number;
|
|
93
|
+
renderMs: number;
|
|
94
|
+
drawMs: number;
|
|
95
|
+
totalMs: number;
|
|
96
|
+
};
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Cost when off
|
|
100
|
+
|
|
101
|
+
`createRuntime` assigns the handler **directly** to `AnimationManager`. When the host clears `runtime.onFrameTiming`, the manager property is `undefined` again. `processFrame` only calls `performance.now()` inside `if (timingHook)` branches.
|
|
102
|
+
|
|
103
|
+
Do **not** wrap with an always-on forwarder (`(s) => host?.(s)`): that would keep the hook truthy and force timing every frame.
|
|
104
|
+
|
|
105
|
+
## Cart-specific helpers
|
|
106
|
+
|
|
107
|
+
This package stays cart-agnostic. Repos that need trait decode beside the bench (e.g. kautomata `hashPerfProbe`) should wrap `benchmarkCartFrames` in the cart tree — not in the engine.
|
package/docs/headless-harness.md
CHANGED
|
@@ -4,7 +4,7 @@ CI / agent wrapper around production `createRuntime({ deterministic })`. Not a s
|
|
|
4
4
|
|
|
5
5
|
Import from **`@cyberart-io/engine/headless`**. Production carts and browser hosts keep using `@cyberart-io/engine` so Vite never resolves `node:fs/promises`.
|
|
6
6
|
|
|
7
|
-
Depends on [deterministic mode](deterministic-mode.md). Host reducers: [events](events.md). Presentation overlay: [presentation adapter](presentation-adapter.md). Several carts: [runtime group](runtime-group.md) (`createHeadlessMultiCartHarness`).
|
|
7
|
+
Depends on [deterministic mode](deterministic-mode.md). Host reducers: [events](events.md). Presentation overlay: [presentation adapter](presentation-adapter.md). Several carts: [runtime group](runtime-group.md) (`createHeadlessMultiCartHarness`). Whole production composition in CI: [production scenario](production-scenario.md). Headless fakes for the framework modules also live on this entry: `createHeadlessAudioAdapter` ([presentation sequences](presentation-sequences.md)), `createHeadlessJobWorker` ([job orchestration](job-orchestration.md)).
|
|
8
8
|
|
|
9
9
|
Back to the [package README](../README.md).
|
|
10
10
|
|
|
@@ -13,10 +13,14 @@ Back to the [package README](../README.md).
|
|
|
13
13
|
| Surface | Import |
|
|
14
14
|
|---|---|
|
|
15
15
|
| Carts, Player, kaleidoscope, `/art` | `@cyberart-io/engine` (`createRuntime`, events, contracts, assets, presentation adapter, presentation cue, capability manifest, geometry, runtime group) |
|
|
16
|
-
| Vitest / jsdom / CI capture | `@cyberart-io/engine/headless` (`createHeadlessHarness`, `createHeadlessMultiCartHarness`, `installHeadlessCanvas`, `captureFrame`) |
|
|
16
|
+
| Vitest / jsdom / CI capture | `@cyberart-io/engine/headless` (`createHeadlessHarness`, `createHeadlessMultiCartHarness`, `installHeadlessCanvas`, `captureFrame`, `benchmarkCartFrames`) |
|
|
17
17
|
|
|
18
18
|
The main export does not re-export the harness. A browser bundle that only imports `@cyberart-io/engine` must not warn about `node:fs/promises`.
|
|
19
19
|
|
|
20
|
+
## Frame performance
|
|
21
|
+
|
|
22
|
+
For update/render timing without mounting a live runtime, use `benchmarkCartFrames` from this entry. For live `rAF` / `step` samples, use `runtime.onFrameTiming` from `@cyberart-io/engine` (unset = no `performance.now` in the draw loop). Full API: [frame benchmarking](frame-benchmark.md).
|
|
23
|
+
|
|
20
24
|
## One-command reproduce (this repo)
|
|
21
25
|
|
|
22
26
|
```bash
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Job orchestration
|
|
2
|
+
|
|
3
|
+
Durable lifecycle for long-running generated work (world chunks, media transforms). The coordinator models queueing, progress, retries, evaluator feedback, leases, and host-owned apply. Workers perform the task; wall-clock completion never mutates authoritative host state.
|
|
4
|
+
|
|
5
|
+
Related: [Events and router](events.md) (CYB-59 contracts), [Replay inspector](replay-inspector.md) (`DEFAULT_SENSITIVE_KEYS` / `REDACTED_VALUE`), [Snapshots](snapshots.md). Built on this: [World patch](world-patch.md) (a `ready` job result becomes one transactional world revision under host `commit`), [World graph](world-graph.md) (generated frontier work). Back to the [package README](../README.md).
|
|
6
|
+
|
|
7
|
+
## One-command reproduce (this repo)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm exec vitest run packages/engine/src/canvas/cyb-80-job-orchestration.repro.spec.ts
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## When to use
|
|
14
|
+
|
|
15
|
+
| Surface | Import | Use |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| Production host | `createJobCoordinator` from `@cyberart-io/engine` | Persist `snapshot()` in envelope `hostState`. Call `accept(jobId)` only under host policy. |
|
|
18
|
+
| Vitest / jsdom | same, or `@cyberart-io/engine/headless` | `createHeadlessJobWorker` controls complete/fail timing. `createMemoryJobPersistence` is enough for refresh tests. |
|
|
19
|
+
|
|
20
|
+
## Contract
|
|
21
|
+
|
|
22
|
+
1. **Definitions** are versioned: request schema, result kind, retry/lease/timeout, declared fallback.
|
|
23
|
+
2. **Submit** with a stable idempotency key and a correlation id for the initiating player intent. Duplicate keys return the same job.
|
|
24
|
+
3. **States:** queued → claimed → awaiting-evaluation → ready → applied, plus retry-scheduled, failed, canceled, superseded.
|
|
25
|
+
4. **Results** are refs (`uri` + `kind`), not embedded media/world payloads.
|
|
26
|
+
5. **Evaluator** may accept, reject, or request a bounded revision.
|
|
27
|
+
6. **Host apply** is `accept(jobId)` on `ready` only. Worker `complete` stops at awaiting-evaluation.
|
|
28
|
+
7. **Restore** validates JSON and schema version before mutate. Tampered snapshots return `{ ok: false }` and leave jobs unchanged.
|
|
29
|
+
8. **Export traces** and replay-inspector payloads redact `JOB_REDACTED_KEYS` / `DEFAULT_SENSITIVE_KEYS` (`prompt`, `credentials`, `credential`, `authorization`, `apiKey`, `secret`) via `REDACTED_VALUE`. `token` is not in that set — pass it in `createReplayInspector({ redactedKeys: ['token'] })` when a cart uses that name. Reason codes stay visible.
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import {
|
|
33
|
+
createHeadlessJobWorker,
|
|
34
|
+
createJobCoordinator,
|
|
35
|
+
} from '@cyberart-io/engine/headless';
|
|
36
|
+
|
|
37
|
+
const worker = createHeadlessJobWorker();
|
|
38
|
+
const jobs = createJobCoordinator({
|
|
39
|
+
definitions: [frontierDefinition],
|
|
40
|
+
worker,
|
|
41
|
+
now: () => clock,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
jobs.submit({
|
|
45
|
+
definitionId: 'frontier-generate',
|
|
46
|
+
idempotencyKey: 'frontier:day-1',
|
|
47
|
+
request: { prompt: '...', seed },
|
|
48
|
+
correlationId: playerIntentId,
|
|
49
|
+
});
|
|
50
|
+
jobs.claim('worker-1');
|
|
51
|
+
worker.complete(jobId, { schemaVersion: 1, kind: 'world-chunk', uri: 'ref://...' });
|
|
52
|
+
jobs.evaluate(jobId, 'accept');
|
|
53
|
+
// host policy: e.g. only at the day boundary
|
|
54
|
+
jobs.accept(jobId);
|
|
55
|
+
```
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Portal lifecycle
|
|
2
|
+
|
|
3
|
+
One enter / exit / abort contract for leaving a cart, inhabiting another exclusively, and returning a versioned outcome. Arcade cabinet, painting, dream, wormhole, book, and nested world are host metaphors over the same primitive — not separate lifecycles.
|
|
4
|
+
|
|
5
|
+
Related: [runtime group](runtime-group.md) (per-participant suspend), [remote cart manifests](remote-cart-manifest.md) (`HostGrantSet` transfer), [snapshots](snapshots.md). Back to the [package README](../README.md).
|
|
6
|
+
|
|
7
|
+
## One-command reproduce (this repo)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm exec vitest run packages/engine/src/canvas/cyb-75-portal-lifecycle.repro.spec.ts
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## When to use
|
|
14
|
+
|
|
15
|
+
| Surface | Import | Use |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| Production host | `createPortalLifecycle`, `cabinetPortal`, `paintingPortal` from `@cyberart-io/engine` | Chrome / back-stack / a11y stay on the host. Carts declare portal targets and outcome schema versions. |
|
|
18
|
+
| Vitest / jsdom | same, or from `@cyberart-io/engine/headless` | Pair with `createRuntimeGroup` after `installHeadlessCanvas()`. |
|
|
19
|
+
|
|
20
|
+
`cabinetPortal` / `paintingPortal` only stamp `metaphor`. They call the same `enter` / `exit` / `abort`.
|
|
21
|
+
|
|
22
|
+
## Contract
|
|
23
|
+
|
|
24
|
+
1. **Enter** another cart (exclusive presentation).
|
|
25
|
+
2. Pass seed/clock/state/persistence and transfer exclusive grants (`audio`, `controller`, `fullscreen`) for the child's lifetime.
|
|
26
|
+
3. **Suspend** the parent: group `step` skips it; cart state stays snapshot-safe.
|
|
27
|
+
4. **Exit** with `{ schemaVersion: 1, kind: 'completed' | 'aborted', payload? }`.
|
|
28
|
+
5. Nested enter is stack-ordered. Aborting the top frame does not rewrite ancestor `parentState`.
|
|
29
|
+
6. Restore validates JSON and participant ids before mutate. A mid-portal snapshot restores parent-suspended + child-active, reapplies extra child grants, and transfers audio broker authorization the same way enter does. Tampered or unknown-id JSON returns `{ ok: false }` and leaves suspend flags and broker grants unchanged.
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import {
|
|
33
|
+
cabinetPortal,
|
|
34
|
+
createPortalLifecycle,
|
|
35
|
+
paintingPortal,
|
|
36
|
+
} from '@cyberart-io/engine';
|
|
37
|
+
|
|
38
|
+
const portal = createPortalLifecycle({
|
|
39
|
+
group,
|
|
40
|
+
grants: { lobby: lobbyGrants, game: gameGrants },
|
|
41
|
+
rootId: 'lobby',
|
|
42
|
+
declarations: [{ id: 'lobby', targets: ['game'] }],
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
cabinetPortal(portal).enter({ from: 'lobby', to: 'game', seed });
|
|
46
|
+
const outcome = portal.exit({ score: 108 });
|
|
47
|
+
paintingPortal(portal).enter({ from: 'lobby', to: 'gallery' });
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Host-owned chrome reads `portal.stack()` and `portal.activeId()`. Persist `portal.snapshot()` inside envelope `hostState`.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Presentation bindings
|
|
2
|
+
|
|
3
|
+
Declarative, versioned mapping from a host-supplied state projection onto presentation resources (visual versions, presence, props, semantic regions, hotspots, sequences, assets). The host reducer stays authoritative. Cyberart evaluates a bounded manifest and applies **one complete presentation revision** — or the declared fail-closed defaults. Never a mixed scene.
|
|
4
|
+
|
|
5
|
+
Related: [Visual layers](visual-layers.md), [Semantic layers](semantic-layers.md), [Presentation sequences](presentation-sequences.md), [Events](events.md), [Snapshots](snapshots.md). Back to the [package README](../README.md).
|
|
6
|
+
|
|
7
|
+
## One-command reproduce (this repo)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm exec vitest run packages/engine/src/canvas/cyb-83-presentation-bindings.repro.spec.ts
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## When to use
|
|
14
|
+
|
|
15
|
+
| Surface | Import | Use |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| Production host | `definePresentationBindings` / `createPresentationBindingRuntime` from `@cyberart-io/engine` | Pass a JSON projection into `apply`. Persist `snapshot()` in envelope `hostState`. |
|
|
18
|
+
| Vitest / jsdom | same, or `@cyberart-io/engine/headless` | Headless and browser entrypoints evaluate the same winners. |
|
|
19
|
+
|
|
20
|
+
The framework does not own inventory, doors, or character location. Hosts define typed projections (CYB-59). Generic fixture ids only: `room`, `overlay`, `whistle`, `door`, `npc`, `lobby`, `hotspot-whistle`, `hotspot-door`, `line-enter`.
|
|
21
|
+
|
|
22
|
+
## Contract
|
|
23
|
+
|
|
24
|
+
1. **Manifest.** `schemaVersion` `1`, stable binding ids, JSON only. Callbacks and `eval` are rejected at define time.
|
|
25
|
+
2. **Predicates.** Constrained expressions over dotted paths (`eq` / `neq` / `present`) or registered typed selectors. No ambient store access.
|
|
26
|
+
3. **Targets.** Visual-layer versions, presence, props, semantic regions, hotspot enablement, sequences, asset bindings.
|
|
27
|
+
4. **Conflicts.** Higher priority wins. Equal priority on the same target is a define-time error. Defaults must cover every target key bindings can select.
|
|
28
|
+
5. **Atomic apply.** Evaluate the full target set, preflight live resources, then apply. `visual.play` / `sequences.playSequence` results are honored: `{ ok: false, reason: 'duplicate' }` is ignored (`onDuplicate: 'ignore'`); other failures abort as `unknown-resource`. On apply failure, controller snapshots are restored and `liveTargets` is not committed. Invalid/missing projections apply the **complete** default set. Restore applies targets before writing revision / selectedBindingIds / override; a failed apply leaves those fields and live presentation unchanged.
|
|
29
|
+
6. **Change minimization.** Unchanged targets are not replayed or remounted. An already-selected matching sequence is not restarted when unrelated projection fields change.
|
|
30
|
+
7. **Overrides.** `setOverride` can change a11y labels / reduced-sensory presentation without mutating the projection or selected binding ids.
|
|
31
|
+
8. **Snapshot.** Records projection revision + selected binding ids + resolved targets. Restore validates schema before mutate.
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import {
|
|
35
|
+
createPresentationBindingRuntime,
|
|
36
|
+
definePresentationBindings,
|
|
37
|
+
} from '@cyberart-io/engine';
|
|
38
|
+
|
|
39
|
+
const defined = definePresentationBindings({
|
|
40
|
+
id: 'lobby-room',
|
|
41
|
+
schemaVersion: 1,
|
|
42
|
+
bindings: [
|
|
43
|
+
{
|
|
44
|
+
id: 'night-room',
|
|
45
|
+
priority: 30,
|
|
46
|
+
when: { path: 'timeOfDay', eq: 'night' },
|
|
47
|
+
targets: [{ kind: 'visual-layer', layerId: 'room', version: 'night' }],
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
defaults: [{ kind: 'visual-layer', layerId: 'room', version: 'day' }],
|
|
51
|
+
});
|
|
52
|
+
if (!defined.ok) throw new Error(defined.errors.map((e) => e.detail).join('; '));
|
|
53
|
+
|
|
54
|
+
const runtime = createPresentationBindingRuntime({
|
|
55
|
+
manifest: defined.manifest,
|
|
56
|
+
visual,
|
|
57
|
+
semantic,
|
|
58
|
+
sequences,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
runtime.apply(
|
|
62
|
+
{ timeOfDay: 'night', inventory: { loonWhistle: false } },
|
|
63
|
+
{ revision: 83 },
|
|
64
|
+
);
|
|
65
|
+
runtime.inspect(); // winners + explanation
|
|
66
|
+
runtime.snapshot(); // revision + selectedBindingIds for hostState
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`createPresentationBindingRuntime` re-validates the manifest and throws if it is invalid.
|
|
70
|
+
|
|
71
|
+
## Predicates
|
|
72
|
+
|
|
73
|
+
| Form | Match |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `{ path: "inventory.loonWhistle", eq: false }` | dotted path equals a JSON primitive |
|
|
76
|
+
| `{ path: "door.roomSeven", neq: "locked" }` | path exists and is not the value, or is missing |
|
|
77
|
+
| `{ path: "timeOfDay", present: true }` | path exists and is not `null` |
|
|
78
|
+
| `{ selector: "isNight" }` | host-registered own-property `(projection) => boolean`. Inherited keys such as `constructor` do not match. |
|
|
79
|
+
| `{ all: [...] }` / `{ any: [...] }` / `{ not: ... }` | boolean combinations |
|
|
80
|
+
|
|
81
|
+
Unknown fields on the manifest, bindings, predicates, or targets fail closed at define time.
|
|
82
|
+
|
|
83
|
+
## Apply, explanation, restore
|
|
84
|
+
|
|
85
|
+
`apply(projection, { revision })` returns `selectedBindingIds`, `explanation` (considered / winners / rejected alternatives / fallback reason), and `inspect()`. Visual swaps use `visual.play` with `durationFrames: 0` so one accepted revision is one coherent layer set. Play results are checked: duplicate-ignore is ok, unknown version / invalid / busy abort and roll the visual / semantic / sequence controllers back. Semantic presence/focus is `setRegionState`. `hitTestEnabled` (and hotspot `enabled` when a `regionId` is set) writes the region's hit-test policy through `semantic.restore`: `false` becomes `pass-through`, `true` restores the region's prior absorbing policy. Sequences use `playSequence` / `skip`. `restore` calls `applyTargets` before committing revision, selected binding ids, or override. `setOverride({ reducedSensory: true })` skips an active bound sequence; clearing the override and reapplying the same projection starts it again.
|
|
86
|
+
|
|
87
|
+
| Event | When |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `presentation.binding.state.applied` | One complete presentation revision committed |
|
|
90
|
+
| `presentation.binding.state.rejected` | Preflight or apply failed; live presentation unchanged |
|
|
91
|
+
| `presentation.binding.diagnostic.lifecycle` | Machine-readable rejection detail |
|
|
92
|
+
|
|
93
|
+
Contracts: `presentationBindingEventContracts()`.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Presentation sequences
|
|
2
|
+
|
|
3
|
+
One frame-stepped clock for a semantic beat that spans audio, captions/text, semantic-region state, visual layers, and typed cues. Hosts pass content through bindings. The engine does not own dialogue graphs, relationship state, inventory, or game rules.
|
|
4
|
+
|
|
5
|
+
Related: [presentation cue](presentation-cue.md), [audio](audio.md), [semantic layers](semantic-layers.md), [visual layers](visual-layers.md), [events](events.md). Back to the [package README](../README.md).
|
|
6
|
+
|
|
7
|
+
## One-command reproduce (this repo)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm exec vitest run packages/engine/src/canvas/cyb-78-presentation-sequence.repro.spec.ts
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## When to use
|
|
14
|
+
|
|
15
|
+
| Surface | Import | Use |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| Production host | `definePresentationSequence` / `createPresentationSequencePlayer` from `@cyberart-io/engine` | Author JSON sequences; `playSequence` with bindings. |
|
|
18
|
+
| Vitest / jsdom | same, plus `createHeadlessAudioAdapter` from `@cyberart-io/engine/headless` | Silent cue traces; `inspectCueIntent` without audible playback. |
|
|
19
|
+
|
|
20
|
+
Generic ids only: `speaker`, `caption`, `room-bg`, `line-1`. Drive `step` from the same clock as deterministic `cart.step`. There are no `setTimeout` / rAF timers.
|
|
21
|
+
|
|
22
|
+
## `definePresentationSequence({ id, tracks, interruptionPolicy, fallbacks })`
|
|
23
|
+
|
|
24
|
+
Authored content must be JSON-serializable. Callbacks and ambient host access are rejected at define time.
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import {
|
|
28
|
+
definePresentationSequence,
|
|
29
|
+
createPresentationSequencePlayer,
|
|
30
|
+
} from '@cyberart-io/engine';
|
|
31
|
+
|
|
32
|
+
const defined = definePresentationSequence({
|
|
33
|
+
id: 'spoken-line',
|
|
34
|
+
interruptionPolicy: 'replace', // 'replace' | 'queue' | 'reject' | 'ignore'
|
|
35
|
+
tracks: [
|
|
36
|
+
{
|
|
37
|
+
id: 'beat',
|
|
38
|
+
kind: 'cue',
|
|
39
|
+
steps: [
|
|
40
|
+
{
|
|
41
|
+
id: 'line-1',
|
|
42
|
+
timing: { kind: 'absolute', atFrame: 0 },
|
|
43
|
+
durationFrames: 6,
|
|
44
|
+
effect: { kind: 'cue', name: 'line-1' },
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'audio',
|
|
50
|
+
kind: 'audio',
|
|
51
|
+
steps: [
|
|
52
|
+
{
|
|
53
|
+
id: 'line-1-audio',
|
|
54
|
+
timing: { kind: 'simultaneous', withStepId: 'line-1', order: 0 },
|
|
55
|
+
durationFrames: 6,
|
|
56
|
+
effect: { kind: 'audio', assetBinding: 'line' },
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
fallbacks: [
|
|
62
|
+
{
|
|
63
|
+
id: 'text-only',
|
|
64
|
+
when: { capability: 'audio', status: 'failed' },
|
|
65
|
+
omitTrackIds: ['audio'],
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Step timing: `absolute` (`atFrame`), `relative` (`afterStepId`, `delayFrames`), or `simultaneous` (`withStepId`, `order`). Completion is `duration` (default) or `immediate`. Simultaneous steps at one frame run in `order`, then `trackId`, then `stepId`. On each `step`, due completions run before new activations so a `relative` follow-up with `delayFrames: 0` does not clobber the finishing step's restore.
|
|
72
|
+
|
|
73
|
+
## `playSequence(sequenceId, { invocationId, bindings })`
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
const player = createPresentationSequencePlayer({
|
|
77
|
+
originFrame: 0,
|
|
78
|
+
audio: headlessAudio, // or createAudioCueTimeline
|
|
79
|
+
semantic,
|
|
80
|
+
visual,
|
|
81
|
+
sequences: [defined.sequence],
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
player.playSequence('spoken-line', {
|
|
85
|
+
invocationId: 'inv-1',
|
|
86
|
+
idempotencyKey: 'inv-1',
|
|
87
|
+
bindings: {
|
|
88
|
+
assets: { line: 'line-1' },
|
|
89
|
+
captions: { line: 'spoken line' },
|
|
90
|
+
regions: { speaker: 'speaker' },
|
|
91
|
+
layers: { room: 'room-bg' },
|
|
92
|
+
versions: { room: 'v1' },
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
player.step(6);
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The player steps bound audio and visual controllers with the same delta so one sequence owns the clock. Caption visibility lives on the player. Semantic focus is `setRegionState`. Visual dim/restore is `visual.play({ kind: 'hide' | 'show' })`. Cue tracks use `createPresentationTimeline`.
|
|
99
|
+
|
|
100
|
+
## Policies
|
|
101
|
+
|
|
102
|
+
| Policy | Behavior |
|
|
103
|
+
|---|---|
|
|
104
|
+
| `skip()` | Restores caption / audio / focus / visual under one policy, emits `skipped`. A second skip is a no-op. The next `playSequence` is available exactly once. |
|
|
105
|
+
| `replay()` | Restarts the last invocation from playhead 0. |
|
|
106
|
+
| `pause()` / `resume()` | Freeze or continue the shared clock. `step` is ignored while paused. |
|
|
107
|
+
| `replace` (default interruption) | Incoming play interrupts the active invocation (`interrupted`) and starts the new one. |
|
|
108
|
+
| `queue` | Incoming play waits until the active invocation completes, skips, or cancels. |
|
|
109
|
+
| `reject` | Incoming play returns `{ ok: false, reason: 'busy' }`. |
|
|
110
|
+
| `ignore` | Incoming play keeps the active invocation. |
|
|
111
|
+
| `cancel()` | Aborts as `interrupted` without semantic completion. |
|
|
112
|
+
| Same `idempotencyKey` | Idempotent: no second `started`. |
|
|
113
|
+
|
|
114
|
+
## Fallbacks and reduced sensory
|
|
115
|
+
|
|
116
|
+
Readiness is probed before `started`. Failed / unavailable / unauthorized / muted audio selects the matching authored fallback (typically `omitTrackIds: ['audio']`) instead of throwing and leaving mixed channels. Semantic steps still complete.
|
|
117
|
+
|
|
118
|
+
`reducedMotion: true` shortens step duration (`skip` / `complete` → 0, or `{ durationFrames: n }`). `reducedSensory: true` omits audio tracks. Caption, focus, and completion still run.
|
|
119
|
+
|
|
120
|
+
## Snapshot / mid-line restore
|
|
121
|
+
|
|
122
|
+
`schemaVersion` is `PRESENTATION_SEQUENCE_SNAPSHOT_SCHEMA_VERSION` (`1`). Restore **fails closed** on unknown version or unknown sequence id and leaves live state unchanged. Unknown queued ids are rejected in that pre-mutation check, before live adapters are released. After mutation, unknown queue rows are skipped rather than failed: `fail()` restores JS backup but does not re-apply adapter effects.
|
|
123
|
+
|
|
124
|
+
Mid-line policy:
|
|
125
|
+
|
|
126
|
+
1. Restore the playhead (`frame`, `startedAtFrame`, completed and active step ids, selected fallback, bindings).
|
|
127
|
+
2. Re-apply currently active step effects (caption visibility, speaker focus, visual hide/show, remaining audio/cue duration).
|
|
128
|
+
3. Do not re-run completed steps.
|
|
129
|
+
4. A **skipped** invocation restores as skipped: remaining duration is not played, channels stay released.
|
|
130
|
+
5. Hosts should pass fresh or already-restored audio / semantic / visual adapters; this module does not seek those clocks backward.
|
|
131
|
+
|
|
132
|
+
## Lifecycle events (CYB-59 contracts)
|
|
133
|
+
|
|
134
|
+
`presentationSequenceEventContracts()` registers:
|
|
135
|
+
|
|
136
|
+
| `type` | When |
|
|
137
|
+
|---|---|
|
|
138
|
+
| `presentation.sequence.state.requested` | `playSequence` accepted |
|
|
139
|
+
| `presentation.sequence.state.preloading` | Capability probe |
|
|
140
|
+
| `presentation.sequence.state.ready` | Fallback selected if needed |
|
|
141
|
+
| `presentation.sequence.state.started` | First steps may apply |
|
|
142
|
+
| `presentation.sequence.state.step-started` | Step becomes active |
|
|
143
|
+
| `presentation.sequence.state.step-completed` | Duration elapsed |
|
|
144
|
+
| `presentation.sequence.state.skipped` | `skip()` |
|
|
145
|
+
| `presentation.sequence.state.interrupted` | replace / cancel |
|
|
146
|
+
| `presentation.sequence.state.failed` | Unrecoverable (reserved) |
|
|
147
|
+
| `presentation.sequence.state.completed` | All steps terminal |
|
|
148
|
+
| `presentation.sequence.diagnostic.lifecycle` | Diagnostics |
|
|
149
|
+
|
|
150
|
+
`inspectCueIntent(sequenceId, { fallbackId })` returns the planned steps without calling the audio adapter.
|