conveyor-engine-assets 0.1.0 → 0.1.1

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.
Files changed (2) hide show
  1. package/README.md +456 -0
  2. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,456 @@
1
+ # conveyor-engine-assets
2
+
3
+ > **Source:** [conveyor-engine-packages on GitHub](https://github.com/autonomous1/conveyor-engine-packages)
4
+
5
+ Deterministic, provenance-aware asset identity, manifests, and loading for Conveyor Engine. It covers the full path from "what is this asset, who made it, what is its content hash" to "the bytes are here, they match the hash, they are decoded, and something holds a reference to them".
6
+
7
+ The package keeps two planes apart:
8
+
9
+ - **Authoritative** assets (collision scenes, rules, spawn metadata, and similar) feed the simulation. They fail closed, never fall back, and drive compatibility hashes.
10
+ - **Presentation** assets (visual scenes, models, materials, textures, audio) only affect what is drawn. They can degrade to a fallback, and changing them never changes an authoritative hash.
11
+
12
+ It has no renderer dependency. GLB files are inspected structurally without `three`, and collision artifacts are flattened into the AABB shape that `conveyor-engine-world` uses.
13
+
14
+ ---
15
+
16
+ ## Contents
17
+
18
+ - [Features](#features)
19
+ - [Installation](#installation)
20
+ - [Dependencies](#dependencies)
21
+ - [Build, typecheck, and test](#build-typecheck-and-test)
22
+ - [Core concepts](#core-concepts)
23
+ - [Module reference](#module-reference)
24
+ - [Usage examples](#usage-examples)
25
+ - [Configuration](#configuration)
26
+ - [Relationship to other packages](#relationship-to-other-packages)
27
+ - [Project layout](#project-layout)
28
+ - [License](#license)
29
+
30
+ ---
31
+
32
+ ## Features
33
+
34
+ - **Strict manifest schema**: `parseManifest` checks every entry's kind, authority, version, `sha256:` content hash, and runtime URI. It also checks dependencies, license, provenance, and runtime policy. It rejects duplicate ids, secrets or credentials anywhere in the document, signed or tokenized URLs, and absolute or workstation paths.
35
+ - **Build-profile evaluation**: `evaluateManifest` applies `development`, `demo`, `release-strict`, `server-authoritative`, and the other profile rules. Examples: missing licenses, missing provenance, or a visual world without an authoritative collision companion.
36
+ - **Portable hashing**: a pure-TypeScript SHA-256 (no `node:crypto`) and canonical JSON (sorted keys). The hasher can be swapped with `setBytesHasher`.
37
+ - **`AssetRuntime` loader**: verifies hashes, decodes JSON, coalesces identical in-flight requests, and keeps a refcounted cache. It supports pinning, preloading, scoped ownership (client, region), cancellation via `AbortSignal`, dependency loading, presentation fallback, typed failure codes, metrics, and lifecycle events.
38
+ - **Pluggable acquisition**: an in-memory provider for tests and fixtures, a filesystem provider with root confinement, an HTTP provider with a host allowlist and timeouts, and `composeProviders` to route between them.
39
+ - **Tick-boundary admission**: `AssetAdmissionQueue` loads and pins static authoritative assets before a session starts (fail closed). It stages `authoritative-admitted` assets off-tick and turns them into replayable admission records at a given tick.
40
+ - **Collision artifacts**: four representations (AABB, triangle mesh, occupancy grid, primitives). Includes validation and flattening into world-compatible XZ obstacles. Render or `three` fields are rejected on collision data.
41
+ - **Static worlds**: resolves a validated `StaticWorldDefinition` (bounds, spawn points, AABBs, and the collision and visual identity) from a manifest plus a collision artifact. Its stable hash ignores visual-only changes.
42
+ - **GLB inspection**: `sniffGlb` parses a GLB container and its glTF JSON into a report covering scenes, nodes, meshes, skins, animations, extensions, and transform sanity, checked against declared renderer capabilities. `encodeGlb` builds GLBs for fixtures.
43
+ - **Presentation descriptors**: render descriptors with primitive fallbacks, skeletal metadata, animation profiles (`HUMANOID_BASIC_V1`), render quality profiles, and lighting defaults.
44
+ - **Pipeline stages**: `createAssetStages` and `runAssetPipeline` provide a four-stage load pipeline. Its stage ids work with `conveyor-graph-compose` style stage lists.
45
+ - **Legacy primitive catalog**: `AssetManifest` maps simple keys such as `primitive/box` to render references, using placeholders for unknown keys.
46
+
47
+ ---
48
+
49
+ ## Installation
50
+
51
+ | Field | Value |
52
+ | --- | --- |
53
+ | `name` | `conveyor-engine-assets` |
54
+ | `version` | `0.0.1` |
55
+ | `type` | `module` (ESM only) |
56
+ | `exports["."]` | `./dist/index.js` (+ `.d.ts`) |
57
+ | `exports["./node"]` | `./dist/node.js` (+ `.d.ts`): filesystem provider |
58
+ | `exports["./http"]` | `./dist/http.js` (+ `.d.ts`): HTTP provider |
59
+ | `files` | `dist`, `src` |
60
+ | `license` | `ISC` |
61
+
62
+ Inside the monorepo, depend on it by path:
63
+
64
+ ```json
65
+ "dependencies": { "conveyor-engine-assets": "file:../conveyor-engine-assets" }
66
+ ```
67
+
68
+ > **Note on entry points.** The root barrel (`src/index.ts`) re-exports `node-fs.ts` and `http-fetch.ts` as well as the rest of the package. `node-fs.ts` imports `node:fs/promises` and `node:path` at module load, so importing the root entry pulls in Node built-ins. The `./node` and `./http` subpaths are narrower entry points. They do **not** make the root entry browser-safe. A browser bundle that imports the root needs those Node modules stubbed or tree-shaken.
69
+
70
+ ---
71
+
72
+ ## Dependencies
73
+
74
+ | Kind | Package | Notes |
75
+ | --- | --- | --- |
76
+ | dependency | `conveyor-engine-core` | `file:../conveyor-engine-core`. Declared in `package.json`, but no file under `src/` currently imports it. |
77
+ | devDependency | `typescript` | `^5.8.2` |
78
+ | devDependency | `@types/node` | `^22.10.0` |
79
+
80
+ Runtime built-ins used: `node:fs/promises` and `node:path` (filesystem provider only), plus global `fetch`, `AbortSignal.timeout`, and optionally `AbortSignal.any` (HTTP provider), and `TextEncoder`/`TextDecoder`.
81
+
82
+ ---
83
+
84
+ ## Build, typecheck, and test
85
+
86
+ ```bash
87
+ npm run build # rm -rf dist && tsc -p tsconfig.json
88
+ npm run typecheck # tsc --noEmit
89
+ npm test # node --experimental-strip-types --import ../test/register.mjs --test test/*.test.ts
90
+ ```
91
+
92
+ The `tsconfig.json` targets ES2022 with `module`/`moduleResolution` `Node16`, `strict`, and declarations, compiling `src/` to `dist/`.
93
+
94
+ How the tests run:
95
+
96
+ - 12 of the 13 test files import `../src/index.ts` directly. They rely on the shared loader hook at the repository root (`../test/register.mjs`), so they need the monorepo layout.
97
+ - `test/static-world.test.ts` imports `../dist/index.js`, so run `npm run build` before `npm test`.
98
+
99
+ ---
100
+
101
+ ## Core concepts
102
+
103
+ ### Authority classes
104
+
105
+ Every `AssetEntry` has an `authority`:
106
+
107
+ | Authority | Plane | Behavior |
108
+ | --- | --- | --- |
109
+ | `authoritative` | simulation | Required by `requireStaticReady`. No fallback. Included in the authoritative compatibility record. |
110
+ | `authoritative-static` | simulation | Same as above. Intended for static collision or world data. |
111
+ | `authoritative-admitted` | simulation | Loaded mid-session through `AssetAdmissionQueue.stage` and committed at a tick by `takeAdmitted`. |
112
+ | `presentation` | render | May degrade to `runtimePolicy.fallbackAssetId`. |
113
+ | `presentation-required` | render | Presentation plane. Included in the presentation compatibility record. |
114
+ | `development` / `diagnostic` | tooling | Not part of either compatibility record. |
115
+
116
+ `AssetRuntime.request` only tries a fallback for entries that are **not** authoritative. For authoritative entries, a hash mismatch or a missing asset always fails.
117
+
118
+ ### Content identity and hashing
119
+
120
+ - `ContentHash` is `sha256:<64 lowercase hex>`, checked by `isContentHash`.
121
+ - `hashBytes`, `hashUtf8`, and `hashCanonicalJson` all go through one swappable hasher (`setBytesHasher(fn)`; pass `undefined` to restore the built-in pure-JS SHA-256).
122
+ - `canonicalJson` sorts object keys recursively. A top-level or array-element `undefined` becomes `null`, and object properties whose value is `undefined` are dropped. A `bigint` becomes its decimal string, and a `Uint8Array` becomes `{ $bytes: <base64> }`. Functions and symbols throw.
123
+
124
+ ### Compatibility records
125
+
126
+ - `collectAuthoritativeCompatibility(manifest)` returns the bundle id, version, protocol and schema versions, and the sorted `(id, version, contentHash, kind, authority)` of every authoritative asset.
127
+ - `collectPresentationCompatibility` does the same for presentation assets.
128
+ - `hashCompatibility` and `authoritativeManifestHash` hash these records. Changing a visual asset never changes the authoritative hash.
129
+
130
+ ### Loader lifecycle
131
+
132
+ 1. `request(id)` looks up the entry. An unknown id fails with `not-found`, and an already-aborted signal fails with `cancelled`.
133
+ 2. The cache key is `id@version#contentHash`. On a cache hit (unless `runtimePolicy.cache === "none"`) the refcount goes up. Identical in-flight loads are coalesced.
134
+ 3. On a miss: dependencies are requested and released first. Then the provider acquires the bytes. The runtime enforces `maxBytes`, verifies the hash, and decodes the payload.
135
+ 4. Decoding produces JSON when the content type contains `json`, the kind is a JSON kind, or the URI ends with `.json`. Otherwise it produces raw bytes.
136
+ 5. The caller gets an `AssetHandle` with `state`, `payload`, `degraded`, `failureCode`, `snapshot()`, and `release()`.
137
+ 6. Releasing the last handle disposes the cache entry unless the asset is pinned (`pin(id)`, `preload`, or `runtimePolicy.pin`).
138
+
139
+ Failures throw `AssetLoadError` with a `code` (`FailureCode`) and an `AssetSnapshot`.
140
+
141
+ ### Admission at tick boundaries
142
+
143
+ `AssetAdmissionQueue` never touches world state. It produces `AdmissionRecord`s (asset, version, hash, authority, tick, outcome, reason) for the caller to apply and replay:
144
+
145
+ - `requireStaticReady()` loads and pins every `authoritative` or `authoritative-static` asset. It throws `StaticAdmissionError` listing the missing ids, and returns the authoritative compatibility record on success.
146
+ - `stage(id)` resolves an asset off-tick. Unknown ids and non-`authoritative-admitted` assets are staged as errors, so they are rejected at the tick.
147
+ - `takeAdmitted(tick, decide?)` turns everything staged into records. An optional `decide` callback can reject individual assets. Rejected handles are released, and accepted ones are pinned.
148
+
149
+ ---
150
+
151
+ ## Module reference
152
+
153
+ ### `types.ts`
154
+
155
+ Shared type vocabulary:
156
+
157
+ - **Asset kinds and roles:** `AssetKind` (20 kinds, from `primitive` and `static-collision-scene` to `skinned-character-model`, `world-manifest`, and `custom`), `AuthorityClass`, and `RuntimeRole`.
158
+ - **Records:** `LicenseRecord` and `LicenseStatus`, `ProvenanceRecord` and `OriginCategory`, `BuildRecord`, `ValidationRecord`, `RuntimePolicy`, `DependencyRef`, `AssetEntry`, and `EngineManifest`.
159
+ - **Compatibility:** `AuthoritativeCompatibility` and `PresentationCompatibility`.
160
+ - **Runtime:** `AssetLifecycleState`, `FailureCode`, `AssetMetrics`, `AssetRuntimeEvent`, `LoadedPayload` (`{kind:"json",value}` or `{kind:"bytes",bytes}`), `AssetSnapshot`, `OwnershipScope` (`consumer`, `clientId`, `regionId`, `entityId`), `AdmissionRecord`, and `BuildProfile`.
161
+ - **Legacy catalog:** `AssetRecord`, `RenderRef`, and `PrimitiveShape` (`box`, `sphere`, `capsule`, `plane`, `line`, `point`).
162
+
163
+ ### `hash.ts`
164
+
165
+ `setBytesHasher`, `isContentHash`, `hashBytes`, `hashUtf8`, `hashCanonicalJson`, `canonicalJson`, and the `BytesHasher` type.
166
+
167
+ ### `schema.ts`
168
+
169
+ - `parseManifest(input)` and `parseManifestJson(text)` throw `ManifestSchemaError`, whose `issues` are `{ path, message }[]`. They require `formatVersion: 1` and a non-empty `bundleId`, `bundleVersion`, and `assets[]`.
170
+ - **Rejected content:**
171
+ - Keys named like secrets anywhere in the document: `accessToken`, `signedUrl`, `apiKey`, `secret`, `password`, `cookie`, `authorization`, `authToken`, `privateKey`, and similar.
172
+ - Signed URLs (`X-Amz-`, `token=`, `sig=`, `signature=`, `expires=`).
173
+ - Absolute paths and workstation paths (`/Users`, `/home`, `/tmp`, `/var/folders`).
174
+ - `authoritativeAssets` and `presentationAssets` filter entries by plane.
175
+ - `collectAuthoritativeCompatibility`, `collectPresentationCompatibility`, and `hashCompatibility` build and hash the compatibility records.
176
+ - `evaluateManifest(manifest, profile = "development")` returns `{ ok, errors, warnings }`. Errors:
177
+ - a missing hash or a missing dependency;
178
+ - an unknown or missing license under `release-strict` or `demo`;
179
+ - missing provenance under `release-strict`;
180
+ - a visual world without an authoritative collision companion under `release-strict` or `server-authoritative`.
181
+
182
+ It also warns when a visual scene is marked `authoritative-static`.
183
+
184
+ ### `acquisition.ts`
185
+
186
+ - `AcquisitionProvider` is `{ id, accepts?(uri), acquire(ctx) }`.
187
+ - `AcquisitionContext` carries `uri`, `runtimeUrl?`, `headers?`, `signal?`, and `timeoutMs?`. `ByteSource` is `{ bytes, uri, contentType? }`.
188
+ - `AcquisitionError` carries a failure `code`.
189
+ - `composeProviders(list)` tries each provider whose `accepts` matches. It rethrows the last error, or throws `provider` if no provider matched.
190
+
191
+ ### `fixtures.ts`
192
+
193
+ - `MemoryAcquisitionProvider`: `register(uri, script | bytes | string)`. It accepts registered URIs and anything starting with `memory:`.
194
+ - A `FixtureScript` can set `delayMs` (abortable), `fail`, `cancel`, or `completeAfterAbort`. With `completeAfterAbort` the fixture finishes its delay and then rejects with `stale` if the caller has aborted.
195
+ - `FixtureRegistry` is a fluent wrapper around the provider: `put`, `success`, and `fail`.
196
+
197
+ ### `node-fs.ts` (also `./node`)
198
+
199
+ - `FilesystemAcquisitionProvider({ root, maxBytes = 32 MiB })` reads files relative to `root`. It accepts scheme-less URIs and the file scheme, and rejects other schemes.
200
+ - It sets the content type for `.json`, `.glb`, and `.gltf`. A missing file maps to `not-found`.
201
+ - `resolveUnderRoot(root, uri)` rejects absolute paths, `..` traversal, and `memory:` or `http:` URIs.
202
+
203
+ ### `http-fetch.ts` (also `./http`)
204
+
205
+ - `HttpAcquisitionProvider({ allowHosts?, fetch?, maxBytes = 32 MiB, defaultTimeoutMs = 30000 })` fetches `ctx.runtimeUrl ?? ctx.uri`, allowing only `http:` and `https:`.
206
+ - With `allowHosts`, hostnames are checked case-insensitively and a miss raises a `policy` error.
207
+ - Size is checked against `Content-Length` and again against the body length.
208
+ - Error mapping: 404 → `not-found`, an aborted caller signal → `cancelled`, a timeout → `timeout`, anything else → `provider`. `FetchLike` lets you inject a fetch implementation.
209
+
210
+ ### `loader.ts`: `AssetRuntime`
211
+
212
+ - **Constructor:** `new AssetRuntime({ manifest, provider, maxBytes = 32 MiB })`.
213
+ - **Requests:** `request(id, opts)`, `getEntry(id)`, and `cacheSize()`.
214
+ - **Pinning:** `pin(id)`, `unpin(id)` (evicts unreferenced entries), and `preload(ids, opts)`, which returns `{ handles, release }` and pins each asset.
215
+ - **Ownership:** `notifyRegionLoaded` and `notifyRegionUnloaded(regionId)`, which releases region-scoped handles. `notifyClientDisconnected(clientId)` releases that client's scoped handles.
216
+ - **Replacement:** `replaceResource(id, nextHash?)` evicts cached versions and emits `resource-replaced`.
217
+ - **Shutdown:** `liveHandleCount()` and `disposeAll()`.
218
+ - **Events and metrics:** `on(listener)` (returns an unsubscribe function), `emit(event)`, and `metrics` (request, coalesce, hit/miss, bytes, ready, degraded, failure, cancellation, hash-mismatch, ownership, release, disposal, pin, and admission counters).
219
+ - **`RequestOptions`:** `signal`, `role`, `allowFallback` (default true), `scope`, `runtimeUrl`, and `headers`.
220
+
221
+ ### `admission.ts`
222
+
223
+ `AssetAdmissionQueue(runtime)`: `requireStaticReady`, `stage`, `peek`, `history`, `takeAdmitted`, and `reject(id, tick, reason?)`. Also exports `StaticAdmissionError` and `StagedAdmission`.
224
+
225
+ ### `collision.ts`
226
+
227
+ - **Artifact shape:** `CollisionArtifact` has `formatVersion: 1`, `representation`, `coordinateSystem` (`y-up` or `z-up`), `unitScale`, `origin`, and `bounds`. Optional fields are `visualAssetId`, `contentHash`, `aabbs`, `mesh`, `grid`, `primitives`, and `spawnPoints`.
228
+ - **Parsing:** `parseCollisionArtifact(input)` throws `CollisionParseError` and rejects render or `three` fields. `collisionFromPayload(payload)` requires a JSON payload.
229
+ - **Validation:** `validateCollisionArtifact(a)` checks bounds, scale and origin finiteness, per-representation structure, mesh index ranges, and grid cell counts. It warns above 250,000 triangles and reports counts.
230
+ - **Flattening:** `collisionToObstacles(a)` returns `{ id, minX, maxX, minZ, maxZ }[]`, the same shape as `conveyor-engine-world`'s `StaticObstacle`. Boxes keep their XZ extent. Spheres and capsules become radius squares. Occupied grid cells become cell boxes, and each mesh triangle becomes its XZ bounding box.
231
+
232
+ ### `static-world.ts`
233
+
234
+ - **Types:** `StaticWorldDefinition` holds bundle identity, schema and protocol version, coordinate system, unit scale, bounds, `SpawnPoint[]`, `aabbs`, collision asset id and hash, the optional visual asset id and hash, and the optional fallback id.
235
+ - **`definitionFromCollision(artifact, opts)`:** spawn points default to the artifact's own (`y` and `yaw` default to 0). `aabbs` are taken **only** from `artifact.aabbs`; other representations produce an empty list here.
236
+ - **Hashing:** `staticWorldCompatibilityRecord(def)` and `hashStaticWorld(def)` cover only authoritative fields, so visual ids do not affect the hash.
237
+ - **Validation:** `validateStaticWorld(def, profile = "minimal" | "example-arena")` checks that spawns are unique, finite, inside the bounds, and not intersecting any AABB (by clearance). It also checks AABB ids are unique and not inverted. `example-arena` additionally requires at least 2 spawns and 4 AABBs. `assertValidStaticWorld` throws `StaticWorldError`.
238
+ - **Manifest lookups:** `collisionAssetFromManifest` and `visualAssetFromManifest` find the authoritative `static-collision-scene` and the presentation `static-visual-scene`.
239
+ - **Resolution:** `resolveStaticWorld({ manifest, collision, profile })` returns `{ definition, hash, collision }`. `collision` may be an artifact, a `LoadedPayload`, or raw JSON. `resolveStaticWorldFromParts({ manifestInput, collisionInput, profile })` parses the manifest first.
240
+ - **Presentation:** `resolvePresentation(visual, load)` maps a visual load outcome to `idle`, `ready`, `degraded` (when a fallback exists), or `failed`. It is presentation-only.
241
+ - **Other exports:** `authoritativeManifestHash` and `parseSpawnPoint`.
242
+
243
+ ### `glb.ts`
244
+
245
+ - `RendererCapabilities` and `DEFAULT_CAPABILITIES`: static meshes on; skins, animations, Draco, KTX2, material extensions, and instancing off.
246
+ - `sniffGlb(bytes, capabilities?)` returns a `GlbValidationReport`:
247
+ - `support`: `static-mesh` or `unsupported`;
248
+ - counts of scenes, nodes, meshes, primitives, materials, textures, images, buffers, accessors, skins, animations, and joints;
249
+ - animation names, used and required extensions, `unsupportedRequired`, scene and node info, `finiteTransforms`, errors, and warnings.
250
+ - It throws `GlbParseError` for a short header, missing magic, a missing JSON chunk, or invalid JSON. A required extension the renderer does not support is an error. Skins or animations without declared support are warnings.
251
+ - `encodeGlb(doc, bin?)` writes a GLB container.
252
+
253
+ ### `render-descriptor.ts`
254
+
255
+ - `parseRenderDescriptor(input)` returns an `EngineRenderDescriptor`:
256
+ - `id`, `modelAssetId`, and the scene and node selectors;
257
+ - `fallback` `{ shape, color?, material? }`, where `shape` defaults to `box`;
258
+ - material class, tint, scale and shadow policies, visibility, instancing, and the animation profile id;
259
+ - `placeholderPolicy` `primitive` or `hide`.
260
+ - `resolveRenderDescriptor(descriptor, report?)` returns either a `glb-node` (scene index and optional node index, looked up by name across the whole file) or a `primitive` fallback with a reason.
261
+ - `parseSkeletalMetadata` handles declarative skeleton metadata (`modelAssetId`, `jointCount`, and so on) with no bone transforms.
262
+
263
+ ### `animation-profile.ts`, `skinned-fixture.ts`
264
+
265
+ - `AnimationProfile` maps the semantics `idle`, `walk`, `run`, and `action-primary` to clip names, along with fades, speeds, and thresholds. `rootMotion` must be `"disabled"`.
266
+ - `validateAnimationProfile(profile, sniffed?)` requires all four aliases. When given a sniffed GLB report, it also requires at least one skin and one animation, and checks that every alias names a real clip.
267
+ - `HUMANOID_BASIC_V1` maps the semantics to the clips `Idle`, `Walk`, `Run`, and `Attack`, with walk speed 2 and run speed 5.
268
+ - `encodeSkinnedDemoGlb()` builds a tiny one-joint, one-mesh GLB with those four clips, for tests. `SKINNED_DEMO_PROFILE` is an alias for `HUMANOID_BASIC_V1`.
269
+
270
+ ### `render-profile.ts`
271
+
272
+ - `RENDER_PROFILES` defines `workstation-high`, `desktop-balanced`, `diagnostic`, and `minimal`. Each sets antialiasing, pixel-ratio cap, tone mapping, exposure, environment and PMREM, light intensities, shadow settings, bloom, clear color, fog, and diagnostics level.
273
+ - `validateRenderProfile(input)` rejects authoritative keys (`tickRate`, `collision`, `replication`, `canonicalSeed`, `authoritativeHash`, `bundleId`) and returns the built-in profile for `id`. Other fields in the input are not merged.
274
+ - `parsePresentationVisual(input)` merges lighting with `DEFAULT_LIGHTING`. `background` defaults to `clear`.
275
+
276
+ ### `compose.ts`
277
+
278
+ - `createAssetStages({ runtime, admission?, sinks?, sniffGlb? })` returns four stages: `resolve-manifest`, `resolve-deps`, `acquire-verify-decode-own`, and `sink`.
279
+ - The acquire stage sniffs GLB bytes for presentation visual kinds and decodes collision JSON for `static-collision-scene` entries.
280
+ - The sink stage calls `onMetrics`, `onFailure`, `onProvenance`, `onPresentationReady`, and `onAuthoritativeReady`. For `authoritative-admitted` assets it calls `admission.stage` and then `onAuthoritativeStaged`.
281
+ - `runAssetPipeline(stages, work, signal?)` runs the stages in order. Thrown errors and aborts become `work.error`; the pipeline does not throw.
282
+ - `loadThroughPipeline(opts, assetId, options?)`, `assetComposeStageSpecs(opts)` (`{ id, handler }[]` for `buildConveyorPipeline({ stages })`), and `staticCompatibilityOf(runtime)`.
283
+
284
+ > The handle obtained by the acquire stage is returned on `work.handle`. The caller owns it and should `release()` it.
285
+
286
+ ### `manifest.ts`
287
+
288
+ `AssetManifest(seed = BUILTIN_PRIMITIVES)`: `register`, `get`, `has`, `keys` (sorted), `resolve(key)` (returns a `RenderRef`, or a box placeholder for unknown keys), and `loadJson`. This is the legacy key-based catalog used for primitive shapes.
289
+
290
+ ---
291
+
292
+ ## Usage examples
293
+
294
+ ### Parse a manifest and load assets from memory
295
+
296
+ ```ts
297
+ import { AssetRuntime, FixtureRegistry, hashBytes, parseManifest } from "conveyor-engine-assets";
298
+
299
+ const bytes = new TextEncoder().encode('{"grid":[1,0,1]}');
300
+ const fixtures = new FixtureRegistry().success("memory:collision", bytes);
301
+
302
+ const manifest = parseManifest({
303
+ formatVersion: 1,
304
+ bundleId: "arena",
305
+ bundleVersion: "1",
306
+ assets: [{
307
+ id: "arena.collision",
308
+ kind: "static-collision-scene",
309
+ authority: "authoritative-static",
310
+ version: "1.0.0",
311
+ contentHash: hashBytes(bytes),
312
+ runtimeUri: "memory:collision",
313
+ license: { id: "CC0-1.0", status: "spdx" },
314
+ }],
315
+ });
316
+
317
+ const runtime = new AssetRuntime({ manifest, provider: fixtures.provider });
318
+ const handle = await runtime.request("arena.collision");
319
+ console.log(handle.state, handle.payload); // "ready", { kind: "json", value: { grid: [1,0,1] } }
320
+ handle.release();
321
+ ```
322
+
323
+ ### Filesystem and HTTP providers together
324
+
325
+ ```ts
326
+ import { AssetRuntime, composeProviders } from "conveyor-engine-assets";
327
+ import { FilesystemAcquisitionProvider } from "conveyor-engine-assets/node";
328
+ import { HttpAcquisitionProvider } from "conveyor-engine-assets/http";
329
+
330
+ const provider = composeProviders([
331
+ new FilesystemAcquisitionProvider({ root: "./assets" }),
332
+ new HttpAcquisitionProvider({ allowHosts: ["cdn.example.com"], defaultTimeoutMs: 10_000 }),
333
+ ]);
334
+ const runtime = new AssetRuntime({ manifest, provider });
335
+ ```
336
+
337
+ ### Fail-closed static readiness, then tick-boundary admission
338
+
339
+ ```ts
340
+ import { AssetAdmissionQueue } from "conveyor-engine-assets";
341
+
342
+ const admission = new AssetAdmissionQueue(runtime);
343
+ const compat = await admission.requireStaticReady(); // throws StaticAdmissionError if anything is missing
344
+
345
+ await admission.stage("arena.rules-patch"); // off-tick
346
+ const records = admission.takeAdmitted(42n); // on the tick boundary
347
+ // apply accepted records to the world yourself and log them for replay
348
+ ```
349
+
350
+ ### Resolve a static world
351
+
352
+ ```ts
353
+ import { resolveStaticWorld, collisionToObstacles } from "conveyor-engine-assets";
354
+
355
+ const { definition, hash, collision } = resolveStaticWorld({
356
+ manifest,
357
+ collision: handle.payload, // LoadedPayload, artifact, or raw JSON
358
+ profile: "example-arena",
359
+ });
360
+ const obstacles = collisionToObstacles(collision); // world StaticObstacle-shaped AABBs
361
+ ```
362
+
363
+ ### Inspect a GLB and pick a render path
364
+
365
+ ```ts
366
+ import { sniffGlb, parseRenderDescriptor, resolveRenderDescriptor } from "conveyor-engine-assets";
367
+
368
+ const report = sniffGlb(glbBytes);
369
+ const desc = parseRenderDescriptor({ id: "crate", modelAssetId: "props.crate", nodeSelector: "Crate", fallback: { shape: "box" } });
370
+ const resolved = resolveRenderDescriptor(desc, report);
371
+ if (resolved.kind === "primitive") console.warn("fallback:", resolved.reason);
372
+ ```
373
+
374
+ ---
375
+
376
+ ## Configuration
377
+
378
+ | Option | Where | Default | Effect |
379
+ | --- | --- | --- | --- |
380
+ | `maxBytes` | `AssetRuntimeOptions` | 32 MiB | Rejects larger payloads with `size-limit`. |
381
+ | `maxBytes` | `FilesystemProviderOptions`, `HttpProviderOptions` | 32 MiB | Provider-level size limit. |
382
+ | `allowHosts` | `HttpProviderOptions` | none (any host) | Host allowlist, compared case-insensitively. |
383
+ | `defaultTimeoutMs` | `HttpProviderOptions` | 30000 | Overridden per entry by `runtimePolicy.timeoutMs`. `<= 0` disables the timeout. |
384
+ | `fetch` | `HttpProviderOptions` | global `fetch` | Injectable `FetchLike`. |
385
+ | `sniffGlb` | `AssetPipelineOptions` | `true` | Whether the acquire stage sniffs presentation GLBs. |
386
+ | `runtimePolicy.cache` | `AssetEntry` | cached | `"none"` skips the cache. `"memory"` and `"refcount"` are accepted but behave the same. |
387
+ | `runtimePolicy.pin` | `AssetEntry` | `false` | Keeps the payload after the last release. |
388
+ | `runtimePolicy.fallbackAssetId` | `AssetEntry` | none | Presentation-only degrade target. |
389
+ | `runtimePolicy.timeoutMs` | `AssetEntry` | provider default | Passed to the provider as `timeoutMs`. |
390
+
391
+ `runtimePolicy.preload`, `runtimePolicy.retryLimit`, and `runtimePolicy.disposeOnRelease` are parsed and validated, but the loader does not act on them. There is no retry loop, and `metrics.retryCount` and each snapshot's `retryCount` stay at 0. Call `preload()` explicitly if you want preloading. `requiredExtensions` on an entry is parsed but not checked against GLB contents; `sniffGlb` checks the GLB's own `extensionsRequired` instead.
392
+
393
+ ---
394
+
395
+ ## Relationship to other packages
396
+
397
+ | Package | Relationship |
398
+ | --- | --- |
399
+ | `conveyor-engine-core` | Declared dependency (`file:../conveyor-engine-core`). No source file imports it today. |
400
+ | `conveyor-engine-world` | Depends on this package. It uses collision artifacts, `collisionToObstacles`, static-world definitions, and compatibility hashing to load authoritative static worlds. |
401
+ | `conveyor-engine-example` | Depends on this package to build the arena manifest, collision, and visual assets and to drive presentation loading. |
402
+ | `conveyor-engine-three` | No direct dependency. Render descriptors, render profiles, and GLB reports are designed as renderer-agnostic inputs for a presentation layer. |
403
+ | `conveyor-graph-compose` | Not a dependency. `assetComposeStageSpecs` produces `{ id, handler }` stages that fit `buildConveyorPipeline({ stages })`. |
404
+
405
+ ---
406
+
407
+ ## Project layout
408
+
409
+ ```
410
+ conveyor-engine-assets/
411
+ ├── package.json, tsconfig.json, README.md
412
+ ├── src/
413
+ │ ├── index.ts # barrel (re-exports everything below, including node-fs and http-fetch)
414
+ │ ├── node.ts # ./node subpath → FilesystemAcquisitionProvider, resolveUnderRoot
415
+ │ ├── http.ts # ./http subpath → HttpAcquisitionProvider
416
+ │ ├── types.ts # kinds, authorities, records, manifest, metrics, events
417
+ │ ├── hash.ts # portable SHA-256, canonical JSON
418
+ │ ├── schema.ts # parseManifest, evaluateManifest, compatibility records
419
+ │ ├── acquisition.ts # provider contract, AcquisitionError, composeProviders
420
+ │ ├── fixtures.ts # MemoryAcquisitionProvider, FixtureRegistry
421
+ │ ├── node-fs.ts # filesystem provider
422
+ │ ├── http-fetch.ts # HTTP provider
423
+ │ ├── loader.ts # AssetRuntime, AssetHandle, AssetLoadError
424
+ │ ├── admission.ts # AssetAdmissionQueue
425
+ │ ├── collision.ts # collision artifacts and flattening
426
+ │ ├── static-world.ts # static world definitions and hashing
427
+ │ ├── glb.ts # sniffGlb, encodeGlb
428
+ │ ├── render-descriptor.ts # render descriptors, skeletal metadata
429
+ │ ├── render-profile.ts # render quality profiles, lighting
430
+ │ ├── animation-profile.ts # animation profiles, HUMANOID_BASIC_V1
431
+ │ ├── skinned-fixture.ts # tiny skinned GLB fixture
432
+ │ ├── compose.ts # pipeline stages
433
+ │ └── manifest.ts # legacy primitive catalog
434
+ └── test/
435
+ ├── admission.test.ts
436
+ ├── animation-profile.test.ts
437
+ ├── assets.test.ts
438
+ ├── collision.test.ts
439
+ ├── compose.test.ts
440
+ ├── glb.test.ts
441
+ ├── loader.test.ts
442
+ ├── ownership.test.ts
443
+ ├── providers.test.ts
444
+ ├── render-profile.test.ts
445
+ ├── schema.test.ts
446
+ ├── skinned-fixture.test.ts
447
+ └── static-world.test.ts
448
+ ```
449
+
450
+ ---
451
+
452
+ ## License
453
+
454
+ ISC. Copyright (c) 2026 autonomous1. See `LICENSE` at the root of the [repository](https://github.com/autonomous1/conveyor-engine-packages).
455
+
456
+ **Repository:** [conveyor-engine-packages on GitHub](https://github.com/autonomous1/conveyor-engine-packages)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conveyor-engine-assets",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Deterministic, provenance-aware asset identity, manifest, and memory loader for Conveyor Engine",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",