From bf92db4b361871cf09fd8114e1f21f39f1e7acb5 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Mon, 26 Oct 2026 01:36:02 -0400 Subject: [PATCH] feat(editor3d): add strict native bindings and API admission contracts --- assets/editor3d/contracts.ts | 21 + docs/scene-editor/architecture.md | 204 ++ examples/editor3d-contracts/README.md | 18 + examples/editor3d-contracts/animation.json | 28 + examples/editor3d-contracts/asset.json | 37 + examples/editor3d-contracts/command.json | 28 + examples/editor3d-contracts/derived.json | 27 + examples/editor3d-contracts/node.json | 33 + examples/editor3d-contracts/project.json | 84 + examples/editor3d-contracts/request.json | 17 + examples/editor3d-contracts/response.json | 20 + examples/editor3d-contracts/revision.json | 18 + examples/editor3d-contracts/scene.json | 37 + examples/editor3d-contracts/session.json | 36 + profiles/editor3d.schema.json | 3266 ++++++++++++++++++++ scripts/editor3d_adapters.py | 176 ++ scripts/editor3d_contracts.py | 504 +++ scripts/generate_schemas.py | 4 +- scripts/test_rig_gate.py | 2 +- tests/test_rig_editor3d_contracts.py | 527 ++++ 20 files changed, 5085 insertions(+), 2 deletions(-) create mode 100644 assets/editor3d/contracts.ts create mode 100644 docs/scene-editor/architecture.md create mode 100644 examples/editor3d-contracts/README.md create mode 100644 examples/editor3d-contracts/animation.json create mode 100644 examples/editor3d-contracts/asset.json create mode 100644 examples/editor3d-contracts/command.json create mode 100644 examples/editor3d-contracts/derived.json create mode 100644 examples/editor3d-contracts/node.json create mode 100644 examples/editor3d-contracts/project.json create mode 100644 examples/editor3d-contracts/request.json create mode 100644 examples/editor3d-contracts/response.json create mode 100644 examples/editor3d-contracts/revision.json create mode 100644 examples/editor3d-contracts/scene.json create mode 100644 examples/editor3d-contracts/session.json create mode 100644 profiles/editor3d.schema.json create mode 100644 scripts/editor3d_adapters.py create mode 100644 scripts/editor3d_contracts.py create mode 100644 tests/test_rig_editor3d_contracts.py diff --git a/assets/editor3d/contracts.ts b/assets/editor3d/contracts.ts new file mode 100644 index 0000000..277117c --- /dev/null +++ b/assets/editor3d/contracts.ts @@ -0,0 +1,21 @@ +// Generated by scripts/editor3d_contracts.py; runtime admission is still required. +export type project = { "schema_version": 1; "kind": "editor3d-project"; "id": string; "name": string; "revision": number; "created_at": string; "updated_at": string; "application": { "name": "hatch-editor3d"; "version": string }; "coordinates": {"handedness":"right","right":"+X","up":"+Y","forward":"+Z","unit":"meter","angles":"radian","quaternion":"XYZW","matrices":"column-vectors","time":"second"}; "color_management": ({ "owner": "rig" | "model3d" | "scene3d" | "production2d" | "camera3d" | "lookdev" | "animation3d"; "id": string; "version": string; "native_kind": string; "file": { "path": string; "sha256": string; "bytes": number; "media_type": "model/gltf-binary" | "model/gltf+json" | "model/obj" | "application/octet-stream" | "application/json" | "image/png" | "image/jpeg" | "image/webp" | "text/plain" | "application/zip" } } | null); "frame_rate": { "numerator": number; "denominator": number }; "scenes": Array<{ "id": string; "sha256": string }>; "shots": Array<{ "owner": "rig" | "model3d" | "scene3d" | "production2d" | "camera3d" | "lookdev" | "animation3d"; "id": string; "version": string; "native_kind": string; "file": { "path": string; "sha256": string; "bytes": number; "media_type": "model/gltf-binary" | "model/gltf+json" | "model/obj" | "application/octet-stream" | "application/json" | "image/png" | "image/jpeg" | "image/webp" | "text/plain" | "application/zip" } }>; "shared_assets": Array<{ "id": string; "sha256": string }>; "characters": Array<{ "owner": "rig" | "model3d" | "scene3d" | "production2d" | "camera3d" | "lookdev" | "animation3d"; "id": string; "version": string; "native_kind": string; "file": { "path": string; "sha256": string; "bytes": number; "media_type": "model/gltf-binary" | "model/gltf+json" | "model/obj" | "application/octet-stream" | "application/json" | "image/png" | "image/jpeg" | "image/webp" | "text/plain" | "application/zip" } }>; "animation_clips": Array<{ "id": string; "sha256": string }>; "render_presets": Array<{ "owner": "rig" | "model3d" | "scene3d" | "production2d" | "camera3d" | "lookdev" | "animation3d"; "id": string; "version": string; "native_kind": string; "file": { "path": string; "sha256": string; "bytes": number; "media_type": "model/gltf-binary" | "model/gltf+json" | "model/obj" | "application/octet-stream" | "application/json" | "image/png" | "image/jpeg" | "image/webp" | "text/plain" | "application/zip" } }>; "editor_metadata": { "label": string; "notes": string }; "provenance": { "origin": "authored" | "imported" | "procedural-fixture"; "fixture": boolean; "description": string; "source_refs": Array<{ "path": string; "sha256": string; "bytes": number; "media_type": "model/gltf-binary" | "model/gltf+json" | "model/obj" | "application/octet-stream" | "application/json" | "image/png" | "image/jpeg" | "image/webp" | "text/plain" | "application/zip" }> }; "dependency_manifest": Array<{ "owner": "rig" | "model3d" | "scene3d" | "production2d" | "camera3d" | "lookdev" | "animation3d"; "id": string; "version": string; "native_kind": string; "file": { "path": string; "sha256": string; "bytes": number; "media_type": "model/gltf-binary" | "model/gltf+json" | "model/obj" | "application/octet-stream" | "application/json" | "image/png" | "image/jpeg" | "image/webp" | "text/plain" | "application/zip" } }>; "source_projects": Array<{ "owner": "rig" | "model3d" | "scene3d" | "production2d" | "camera3d" | "lookdev" | "animation3d"; "id": string; "version": string; "native_kind": string; "file": { "path": string; "sha256": string; "bytes": number; "media_type": "model/gltf-binary" | "model/gltf+json" | "model/obj" | "application/octet-stream" | "application/json" | "image/png" | "image/jpeg" | "image/webp" | "text/plain" | "application/zip" } }>; "validation_status": "unvalidated" | "valid" | "invalid"; "review_status": "unreviewed" }; +export type scene = { "schema_version": 1; "kind": "editor3d-scene"; "id": string; "project_id": string; "native": { "owner": "rig" | "model3d" | "scene3d" | "production2d" | "camera3d" | "lookdev" | "animation3d"; "id": string; "version": string; "native_kind": string; "file": { "path": string; "sha256": string; "bytes": number; "media_type": "model/gltf-binary" | "model/gltf+json" | "model/obj" | "application/octet-stream" | "application/json" | "image/png" | "image/jpeg" | "image/webp" | "text/plain" | "application/zip" } }; "nodes": Array<{ "id": string; "sha256": string }>; "collections": Array; "default_camera": (string | null); "animations": Array; "timeline": { "start": number; "end": number }; "editor_metadata": { "label": string; "notes": string } }; +export type node = { "schema_version": 1; "kind": "editor3d-node"; "id": string; "scene_id": string; "native": { "owner": "rig" | "model3d" | "scene3d" | "production2d" | "camera3d" | "lookdev" | "animation3d"; "id": string; "version": string; "native_kind": string; "file": { "path": string; "sha256": string; "bytes": number; "media_type": "model/gltf-binary" | "model/gltf+json" | "model/obj" | "application/octet-stream" | "application/json" | "image/png" | "image/jpeg" | "image/webp" | "text/plain" | "application/zip" } }; "native_node_id": string; "node_type": "mesh" | "group" | "character" | "armature" | "bone" | "camera" | "point-light" | "directional-light" | "spot-light" | "area-light" | "world" | "primitive" | "instance"; "parent": (string | null); "sibling_order": number; "selectable": boolean; "locked": boolean; "collections": Array; "asset_id": (string | null); "rig_id": (string | null); "editor_metadata": { "label": string; "notes": string } }; +export type asset = { "schema_version": 1; "kind": "editor3d-asset"; "id": string; "native": { "owner": "rig" | "model3d" | "scene3d" | "production2d" | "camera3d" | "lookdev" | "animation3d"; "id": string; "version": string; "native_kind": string; "file": { "path": string; "sha256": string; "bytes": number; "media_type": "model/gltf-binary" | "model/gltf+json" | "model/obj" | "application/octet-stream" | "application/json" | "image/png" | "image/jpeg" | "image/webp" | "text/plain" | "application/zip" } }; "asset_type": "model3d" | "rig" | "image2d" | "reference2d" | "material" | "texture" | "camera" | "animation"; "original_filename": string; "original_format": string; "normalized_format": string; "provenance": { "origin": "authored" | "imported" | "procedural-fixture"; "fixture": boolean; "description": string; "source_refs": Array<{ "path": string; "sha256": string; "bytes": number; "media_type": "model/gltf-binary" | "model/gltf+json" | "model/obj" | "application/octet-stream" | "application/json" | "image/png" | "image/jpeg" | "image/webp" | "text/plain" | "application/zip" }> }; "license": { "identifier": (string | null); "attribution": (string | null); "permission": "unknown" | "restricted" | "permitted"; "evidence": ({ "path": string; "sha256": string; "bytes": number; "media_type": "model/gltf-binary" | "model/gltf+json" | "model/obj" | "application/octet-stream" | "application/json" | "image/png" | "image/jpeg" | "image/webp" | "text/plain" | "application/zip" } | null) }; "dependencies": Array<{ "owner": "rig" | "model3d" | "scene3d" | "production2d" | "camera3d" | "lookdev" | "animation3d"; "id": string; "version": string; "native_kind": string; "file": { "path": string; "sha256": string; "bytes": number; "media_type": "model/gltf-binary" | "model/gltf+json" | "model/obj" | "application/octet-stream" | "application/json" | "image/png" | "image/jpeg" | "image/webp" | "text/plain" | "application/zip" } }>; "thumbnails": Array<{ "path": string; "sha256": string; "bytes": number; "media_type": "model/gltf-binary" | "model/gltf+json" | "model/obj" | "application/octet-stream" | "application/json" | "image/png" | "image/jpeg" | "image/webp" | "text/plain" | "application/zip" }>; "validation_status": "unvalidated" | "valid" | "invalid"; "previous": ({ "owner": "rig" | "model3d" | "scene3d" | "production2d" | "camera3d" | "lookdev" | "animation3d"; "id": string; "version": string; "native_kind": string; "file": { "path": string; "sha256": string; "bytes": number; "media_type": "model/gltf-binary" | "model/gltf+json" | "model/obj" | "application/octet-stream" | "application/json" | "image/png" | "image/jpeg" | "image/webp" | "text/plain" | "application/zip" } } | null) }; +export type animation = { "schema_version": 1; "kind": "editor3d-animation"; "id": string; "native": { "owner": "rig" | "model3d" | "scene3d" | "production2d" | "camera3d" | "lookdev" | "animation3d"; "id": string; "version": string; "native_kind": string; "file": { "path": string; "sha256": string; "bytes": number; "media_type": "model/gltf-binary" | "model/gltf+json" | "model/obj" | "application/octet-stream" | "application/json" | "image/png" | "image/jpeg" | "image/webp" | "text/plain" | "application/zip" } }; "targets": Array; "frame_rate": { "numerator": number; "denominator": number }; "interval": { "start": number; "end": number } }; +export type command = { "schema_version": 1; "kind": "editor3d-command"; "id": string; "project_id": string; "scene_id": string; "expected_revision": number; "expected_project_sha256": string; "transaction_id": string; "operation": "set-transform" | "reparent" | "set-metadata" | "create-node" | "delete-node" | "set-native-property" | "replace-animation"; "targets": Array; "input": { "owner": "rig" | "model3d" | "scene3d" | "production2d" | "camera3d" | "lookdev" | "animation3d"; "id": string; "version": string; "native_kind": string; "file": { "path": string; "sha256": string; "bytes": number; "media_type": "model/gltf-binary" | "model/gltf+json" | "model/obj" | "application/octet-stream" | "application/json" | "image/png" | "image/jpeg" | "image/webp" | "text/plain" | "application/zip" } }; "inverse": ({ "owner": "rig" | "model3d" | "scene3d" | "production2d" | "camera3d" | "lookdev" | "animation3d"; "id": string; "version": string; "native_kind": string; "file": { "path": string; "sha256": string; "bytes": number; "media_type": "model/gltf-binary" | "model/gltf+json" | "model/obj" | "application/octet-stream" | "application/json" | "image/png" | "image/jpeg" | "image/webp" | "text/plain" | "application/zip" } } | null); "author": string }; +export type revision = { "schema_version": 1; "kind": "editor3d-revision"; "id": string; "project_id": string; "number": number; "parent": ({ "id": string; "sha256": string } | null); "project_sha256": string; "command_ids": Array; "created_at": string; "author": string; "snapshot": { "path": string; "sha256": string; "bytes": number; "media_type": "model/gltf-binary" | "model/gltf+json" | "model/obj" | "application/octet-stream" | "application/json" | "image/png" | "image/jpeg" | "image/webp" | "text/plain" | "application/zip" } }; +export type session = { "schema_version": 1; "kind": "editor3d-session"; "id": string; "project_id": string; "base_revision": number; "base_project_sha256": string; "selection": Array; "active_scene": (string | null); "playhead": number; "viewport": { "translation": [number, number, number]; "rotation": [number, number, number, number]; "scale": [number, number, number] }; "overlays": Array<"grid" | "bones" | "bounds" | "selection" | "gizmo">; "tool": "select" | "translate" | "rotate" | "scale"; "unsaved_command_ids": Array }; +export type derived = { "schema_version": 1; "kind": "editor3d-derived"; "project_id": string; "project_sha256": string; "frame": number; "frame_rate": { "numerator": number; "denominator": number }; "native_dependencies": Array<{ "owner": "rig" | "model3d" | "scene3d" | "production2d" | "camera3d" | "lookdev" | "animation3d"; "id": string; "version": string; "native_kind": string; "file": { "path": string; "sha256": string; "bytes": number; "media_type": "model/gltf-binary" | "model/gltf+json" | "model/obj" | "application/octet-stream" | "application/json" | "image/png" | "image/jpeg" | "image/webp" | "text/plain" | "application/zip" } }>; "evidence": Array<{ "path": string; "sha256": string; "bytes": number; "media_type": "model/gltf-binary" | "model/gltf+json" | "model/obj" | "application/octet-stream" | "application/json" | "image/png" | "image/jpeg" | "image/webp" | "text/plain" | "application/zip" }>; "status": "unavailable" | "stale" | "verified" }; +export type request = { "schema_version": 1; "kind": "editor3d-request"; "id": string; "operation": "projects.list" | "projects.open" | "projects.get" | "projects.validate" | "revisions.list" | "scenes.list" | "scenes.get" | "scenes.validate" | "assets.list" | "assets.get" | "assets.content" | "assets.validate" | "assets.dependencies" | "render.capabilities" | "render.status" | "render.progress" | "render.manifest" | "render.output" | "recovery.inspect" | "recovery.list" | "recovery.compare" | "recovery.dependencies" | "projects.create" | "projects.save" | "projects.close" | "revisions.create" | "revisions.restore" | "scenes.create" | "scenes.command" | "scenes.delete" | "assets.import" | "assets.thumbnail" | "assets.relink" | "render.create" | "render.cancel" | "render.retry" | "recovery.recover"; "project_id": (string | null); "target_id": (string | null); "expected_revision": (number | null); "expected_project_sha256": (string | null); "argument": ({ "path": string; "sha256": string; "bytes": number; "media_type": "model/gltf-binary" | "model/gltf+json" | "model/obj" | "application/octet-stream" | "application/json" | "image/png" | "image/jpeg" | "image/webp" | "text/plain" | "application/zip" } | null); "idempotency_key": (string | null) }; +export type response = { "schema_version": 1; "kind": "editor3d-response"; "request_id": string; "status": "accepted" | "complete" | "rejected" | "unavailable" | "cancelled"; "project_revision": (number | null); "project_sha256": (string | null); "outputs": Array<{ "path": string; "sha256": string; "bytes": number; "media_type": "model/gltf-binary" | "model/gltf+json" | "model/obj" | "application/octet-stream" | "application/json" | "image/png" | "image/jpeg" | "image/webp" | "text/plain" | "application/zip" }>; "diagnostics": Array<{ "code": string; "path": string; "message": string; "correction": string; "retryable": boolean }>; "executed": boolean; "review_status": "unreviewed" }; +export type NativeReference = { "owner": "rig" | "model3d" | "scene3d" | "production2d" | "camera3d" | "lookdev" | "animation3d"; "id": string; "version": string; "native_kind": string; "file": { "path": string; "sha256": string; "bytes": number; "media_type": "model/gltf-binary" | "model/gltf+json" | "model/obj" | "application/octet-stream" | "application/json" | "image/png" | "image/jpeg" | "image/webp" | "text/plain" | "application/zip" } }; +export type FileReference = { "path": string; "sha256": string; "bytes": number; "media_type": "model/gltf-binary" | "model/gltf+json" | "model/obj" | "application/octet-stream" | "application/json" | "image/png" | "image/jpeg" | "image/webp" | "text/plain" | "application/zip" }; +export type ResourcePointer = { "id": string; "sha256": string }; +export type FrameRate = { "numerator": number; "denominator": number }; +export type FrameInterval = { "start": number; "end": number }; +export type EditorMetadata = { "label": string; "notes": string }; +export type Diagnostic = { "code": string; "path": string; "message": string; "correction": string; "retryable": boolean }; +export type Provenance = { "origin": "authored" | "imported" | "procedural-fixture"; "fixture": boolean; "description": string; "source_refs": Array<{ "path": string; "sha256": string; "bytes": number; "media_type": "model/gltf-binary" | "model/gltf+json" | "model/obj" | "application/octet-stream" | "application/json" | "image/png" | "image/jpeg" | "image/webp" | "text/plain" | "application/zip" }> }; +export type Transform = { "translation": [number, number, number]; "rotation": [number, number, number, number]; "scale": [number, number, number] }; diff --git a/docs/scene-editor/architecture.md b/docs/scene-editor/architecture.md new file mode 100644 index 0000000..1d5d65f --- /dev/null +++ b/docs/scene-editor/architecture.md @@ -0,0 +1,204 @@ +# Editor3D contract foundation + +This document describes the bounded #99 foundation at main baseline +`bb5a6810afd06cf57185ffdfe2234e0fdc6e6240`. It does not describe a running +scene editor. Persistence/undo/autosave (#100), browser authoring (#101), asset +workflows (#102), inspectors (#103), timelines (#104), rendering (#105), and +end-to-end/security/browser acceptance (#106) remain separate requirements. +The full parent #17 remains open. + +## Repository audit and integration ownership + +The baseline contains no persistent general-purpose 3D scene editor, React +application, scene database, or editor command service. There is no tracked +AGENTS.md or CONTRIBUTING.md on this baseline. SKILL.md retains the compatible +`hatch-pet` name and requires source-bound review, optional 3D isolation and +separate engineering/production evidence. Existing delivery media was recovered; +the historical missing-media premise is not a current blanket failure claim. + +| Existing owner | Actual behavior and reuse boundary | +| --- | --- | +| `scripts/hatch.py`, `doctor.py`, `profile_spec.py` | Existing workflow/CLI and dependency-free capability discovery; profiles retain fixed Codex atlas and existing 2D behavior. New editor imports are not added to these paths. | +| `production.py`, `production_contracts.py` | CLI and immutable portable project/cast/clip/scene/shot references. These scenes describe layered 2D performances, not a 3D graph. `load_project` verifies exact dependency hashes and explicit missing-resource diagnostics. | +| `production_performance.py`, `production_render.py` | Integer-frame evaluation and deterministic raster composition; browser playback is not the authoritative clock. No 3D image result is inferred from these outputs. | +| `production_workflow.py` | Existing source/acquisition claims, dependency impact, append-only scoped review and portable archive lifecycle. These remain the authority for original 2D assets/reviews, not rewritten as editor approvals. | +| `production_sources.py`, `production_bundle.py` | Exact source delivery and archive reconstruction; source bytes and hashes stay untouched. | +| `production_adapter.py` | Actual PNG/WAV/OpenRaster/Krita handoff, distinct from editable 3D scene data and native application acceptance. | +| `review_ui.py`, `assets/review.html`, `assets/playback.js` | Offline artifact review and playback, not general scene authoring. | +| `decomposition_spec.py`, `character_decomposition.py`, `decomposition_review.py` | Source-bound visible-pixel parts and semantic review; neither masks nor 2D layers become a mesh by linking them. | +| `rig_contracts.py`, `rig_models.py`, `rig_math.py` | Actual rig/model/skin/animation contracts and implementation; right-handed +Y-up, +Z-forward world, meters, XYZW, column-vector transforms. This is the coordinate authority. | +| `rig_http.py`, `rig_jobs.py`, `rig_operations.py` | Existing loopback bearer/origin/body limits and serialized native job operations. The editor's admission helpers do not add routes to this server or weaken its checks. | +| `rig_viewer.py`, `assets/rig-viewer/` | Actual Three.js rig preview, controls and animation inspection. It is not yet an editor for multi-scene projects. | +| `model3d_contracts.py`, `model3d_validation.py` | Strict source/model references plus actual file inspection. New editor references reuse their portable file syntax; manifest validation is not geometry acceptance. | +| Scene3D PR #210 / issue #59 | Active independent scene contract owner, absent from this main baseline. Editor scene/node wrappers pin its future native resources. They do not create a competing scene format or copy its implementation. | +| Camera3D, LookDev, Animation3D owners | Native camera/light/material/animation behavior stays outside this editor envelope. A missing owner fails explicitly. | + +The expected flow is native owner documents → hash-bound editor bindings → +validated command request → native owner transaction → new immutable document +revision → derived viewport/render projections. A future service must revalidate +native resources and command preconditions inside the owner's transaction. +No current helper performs that transaction. + +## Implemented shared contracts + +`scripts/editor3d_contracts.py` is the single source for +`profiles/editor3d.schema.json` and `assets/editor3d/contracts.ts`. The central +schema generator also registers the JSON schema. TypeScript types are generated +from these definitions, not independently handwritten. They express structural +shape; runtime constraints (hash closure, cycles, rational reduction, native +ownership) still require runtime admission. + +Eleven roots use `schema_version: 1` and the `editor3d-` namespace: + +| Root | Meaning | +| --- | --- | +| `project` | Stable identity, revision/timestamps, app version, fixed coordinates, rational clock, exact pointers to editor bindings, native shots/characters/color/render resources, provenance and independent review state. | +| `scene` | Editor projection of one native scene: native reference, ordered node hashes, collections, camera selection and half-open timeline. It contains no parallel scene mesh/world/material model. | +| `node` | Stable binding to a native node, scene/hierarchy/order and editor selection/lock metadata. The native owner retains transform/mesh/material/constraint payloads. Supported bindings distinguish meshes, groups, characters, armatures, bones, cameras, physical light kinds, worlds, primitives and instances. | +| `asset` | Exact native manifest and provenance/license metadata, original/normalized format, dependency and thumbnail references, validation state and previous version. Filename equality never establishes asset identity. | +| `animation` | Exact native clip plus editor target bindings and half-open frame interval. Native skeletal/property channels are not redefined. | +| `command` | Targeted operation, native input/inverse resources, expected project hash/revision and transaction ID. This is an intent envelope, not an executed edit or undo implementation. | +| `revision` | Parent pointer, immutable project hash, command IDs, author/timestamp and exact snapshot file. It is a record contract, not a revision repository. | +| `session` | Nonpersistent selection, active scene, playhead, viewport, tool, overlays and unsaved command IDs, tied to a saved hash/revision. Tokens and machine paths have no portable fields. | +| `derived` | Nonpersistent frame projection/cache evidence tied to an exact saved project and native dependencies. No geometry is inferred from an empty or unavailable projection. | +| `request` | Typed service operation, exact project/resource scope, argument file, mutation preconditions and idempotency key. | +| `response` | Request outcome, optional exact project revision, file evidence and actionable diagnostics. Admission never emits `complete`; response validation alone cannot prove an operation occurred. | + +The CLI opens a bounded regular descriptor with nonblocking/no-follow flags where +available, rejects static symlink inputs, and limits the read independently of +reported size. It does not claim transactional protection against concurrent +ancestor replacement on platforms without no-follow support. + +The native reference consists of `owner`, native `id`, exact `version`, +`native_kind`, and `file` (portable path, SHA-256, byte length, media type). +Binding pointers use the canonical semantic JSON hash returned by `contract_hash`; +file references use SHA-256 of actual file bytes. These hashes are intentionally +different and must not be substituted. A native format with only `schema_version` +uses its format version `N.0.0`; formats carrying `version` retain that version. + +The fixture project binds the real retained Model3D robotic manifest. Its +scene3d reference deliberately names an unavailable owner and nonexistent file. +`validate_project_bundle` validates editor closure only and explicitly returns +`native_resources_verified: false`. `verify_native_reference` rejects this +missing owner before opening a path. This is not a scene, model generation, +interactive browser demonstration, or accepted production asset. + +## Runtime admission and invariants + +`validate_contract` rejects unknown root kinds/versions/fields, duplicate source +JSON keys (through `decode_json`), invalid hashes/paths, nonfinite values, excessive +structural arrays, malformed or offset-free timestamps (validated with the standard library without +optional RFC3339 checker dependencies; supported timestamps use at most six +fractional digits and no leap-second value), nonsingular-scale violations in math, +non-unit quaternions, non-reduced rates, and empty/reversed intervals. JSON Schema +permits integral JSON numbers such as `24.0`; arithmetic normalizes them while +preserving original bytes and hashes. + +`validate_project_bundle` checks exact pointer coverage, unique IDs, same-scene +parent membership, acyclic parent chains, distinct sibling order, collections, +asset and rig references, camera node type and animation target existence. It +never checks the truth of projected relationships against an unavailable native +scene. The future owner adapter must do that before opening/editing/rendering. + +`validate_runtime_state` checks session/project identity and hash/revision, +selection membership and active-scene playhead bounds. Derived clocks must match +the project and their frame must lie in at least one saved scene range, including +its terminal boundary. Both contexts reject nodes outside the saved scene closure. Persistent serialization +has no fields for session/derived state, so loading such fields into a project is +an error rather than silent data loss. + +`migrate_document` is an identity copy for validated v1 documents only. No +unsupported-version coercion or destructive legacy migration is implemented. + +## Coordinate and clock adapters + +`frame_time(frame, fps)` returns an exact `Fraction` in seconds. Schema-valid +integral JSON boundaries (for example `720.0`) are normalized for arithmetic. A 24/1 clock at +boundary 720 is exactly 30 seconds. Renderable frames in `[0,720)` are 0–719; +720 remains a terminal boundary. The editor session may seek that boundary. +Existing millisecond animation and 2D integer-frame contracts remain unchanged. + +`transform_to_blender` lazily delegates matrix construction to `rig_math` and +conjugates object transforms by its canonical-to-Blender basis. It does not +mutate saved coordinates. Camera transforms require the camera owner's local +forward/up adapter and are not handled by this generic object helper. +`euler_to_quaternion`/`quaternion_to_euler` explicitly use fixed-axis extrinsic XYZ +radians (`Rz @ Ry @ Rx`); the latter reports a gimbal-lock warning and gives a +canonical equivalent rotation. Quaternion data remains authoritative. +World-preserving reparenting, shear handling and authored hierarchy execution +remain #100/#101/native scene work, not behavior of the binding validator. + +## Local API policy and exact operation contracts + +`validate_api_request` is executable preflight for original UTF-8 JSON bytes. +It authenticates a minimum 24-character bearer token with constant-time comparison, +requires an exact configured HTTP loopback origin/port (no cross-origin browser +access), application/json, and exact Content-Length of 1–4 MiB. Native paths never +arrive as arbitrary local absolute identifiers. A service must reject duplicate +HTTP headers, bind loopback, bound sockets/workers, and read no more than the +admitted length. The helper does not start a server or issue any network request. + +Read operations: `projects.list/open/get/validate`, `revisions.list`, +`scenes.list/get/validate`, `assets.list/get/content/validate/dependencies`, +`render.capabilities/status/progress/manifest/output`, and +`recovery.inspect/list/compare/dependencies`. They accept no mutation preconditions, +idempotency key or input file and must have no persistent side effect. + +Mutation operations: `projects.create/save/close`, `revisions.create/restore`, +`scenes.create/command/delete`, `assets.import/thumbnail/relink`, +`render.create/cancel/retry`, and `recovery.recover`. They require an idempotency +key and, except create, a project ID, expected revision and expected canonical +project hash matching the supplied current snapshot. A service must recheck under +the native owner lock. Rejection preserves the saved state; conflict recovery is +reload, compare and explicit reapplication, never overwrite. + +The four global operations `projects.list`, `projects.create`, +`render.capabilities`, `recovery.list` omit project ID; all others require it. +Resource-targeted operations require `target_id`; project-level open/get/validate/ +save/close, list operations, revision create, scene create, asset import, render +create and recovery inspect/dependencies omit it. An exact input-file argument is +required only for project create/save, scene create/command, asset import/relink, +render create/retry and recovery recover. Arguments remain untrusted until their +native owner validates their bytes and semantics. + +Responses distinguish accepted work, actual completion, cancellation, rejection +and unavailability. Failed/unavailable results require a diagnostic with an +object path and recovery guidance. The runtime preflight returns only +`admitted, executed: false`; no success handler, job scheduler, cancellation, +streaming, retry, or export verification is implemented here. Future long-running +operations must return a job identity promptly and support bounded polling; SSE +or WebSockets must not bypass identical authorization/origin/revision controls. + +## Validation and evidence + +Reproduce generated artifacts and standalone contract admission: + +```bash +python scripts/editor3d_contracts.py --check +python scripts/generate_schemas.py --check +python scripts/editor3d_contracts.py --validate examples/editor3d-contracts/project.json +PYTHONPATH=scripts:tests python -m unittest test_rig_editor3d_contracts -v +python scripts/test_rig_gate.py schema-math +``` + +The existing schema-math gate includes the editor contract module; it stays out +of 2D-only discovery because its math probes use optional NumPy. The new tests +exercise structural parity, all root fixtures, adversarial schema +and semantic inputs, exact bundle closure, real native Model3D manifest admission, +changed bytes, API preflight rejection, coordinate rotation/basis checks and exact +rational timing. They do not count a static TypeScript file as a running editor, +or a contract fixture as native scene rendering. Baseline/test results and +remaining environment blockers belong to the revision-bound evidence receipt +published with this change; historical DELIVERY_STATUS results are not fresh +results for this patch. + +| #99 requirement | Implementation | Remaining boundary | +| --- | --- | --- | +| Audit current frontend/backend/assets/review/history and baseline | This audit, inherited owners, independent baseline logs | No claim of complete existing full-suite rerun unless the receipt says so | +| Shared project/scene/node/asset/animation/command/revision types and equivalence | Single schema authority, generated TS, strict admission, hash closure | Native scene owner semantics and broad editor state required by parent §5 remain blocked on #59/#210 and runtime work | +| Persistent/session/derived separation, stable IDs, coordinate/time adapters | Closed roots, context checks, existing math and exact fractions | Scene evaluation, world-preserving reparenting, UI persistence remain separate | +| Local API safety/revision/integration specification | Request/response types, preflight, operation matrix, native validators | No HTTP server, filesystem mutation, render job or editor control is represented as delivered | + +A future integration must preserve the existing 2D, style/state, decomposition, +source/review and portable-package tests. No browser support, real Blender scene +rendering, production visual approval, or end-to-end editor acceptance is claimed. diff --git a/examples/editor3d-contracts/README.md b/examples/editor3d-contracts/README.md new file mode 100644 index 0000000..193c259 --- /dev/null +++ b/examples/editor3d-contracts/README.md @@ -0,0 +1,18 @@ +# Editor contract fixtures + +These self-authored engineering fixtures exercise the #99 editor binding contract. +They are not an editable scene, browser demo or accepted production artwork. + +`project.json`, `scene.json`, `node.json`, and `asset.json` form an exact editor +binding closure. The asset pins actual bytes from the retained Model3D robotic +manifest. The scene pins an intentionally unavailable Scene3D owner; the absent +`unavailable/scene.json` must never be generated or relabeled as a native scene. +`animation.json` is a standalone root example not bound in that base project. +The other roots illustrate intent/revision/session/derived/service records; +`response.json` explicitly records unavailable execution. + +Run `python scripts/editor3d_contracts.py --validate FILE` for one root. Run +`test_rig_editor3d_contracts` for relational closure and negative controls. Structural +admission and exact JSON hashes cannot establish native resource truth, actual +persistence, rendering, browser behavior, source licensing or visual approval. +See `docs/scene-editor/architecture.md` for authority and operation boundaries. diff --git a/examples/editor3d-contracts/animation.json b/examples/editor3d-contracts/animation.json new file mode 100644 index 0000000..d682bc0 --- /dev/null +++ b/examples/editor3d-contracts/animation.json @@ -0,0 +1,28 @@ +{ + "schema_version": 1, + "kind": "editor3d-animation", + "id": "animation", + "native": { + "owner": "scene3d", + "id": "native-scene", + "version": "1.0.0", + "native_kind": "scene3d-scene", + "file": { + "path": "unavailable/scene.json", + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "bytes": 1, + "media_type": "application/json" + } + }, + "targets": [ + "node" + ], + "frame_rate": { + "numerator": 24, + "denominator": 1 + }, + "interval": { + "start": 0, + "end": 720 + } +} diff --git a/examples/editor3d-contracts/asset.json b/examples/editor3d-contracts/asset.json new file mode 100644 index 0000000..1491314 --- /dev/null +++ b/examples/editor3d-contracts/asset.json @@ -0,0 +1,37 @@ +{ + "schema_version": 1, + "kind": "editor3d-asset", + "id": "asset", + "native": { + "owner": "model3d", + "id": "robotic-contract-example", + "version": "1.0.0", + "native_kind": "model3d-asset", + "file": { + "path": "examples/model3d-contracts/robotic.json", + "sha256": "ce666b1b75c7b183007886e5674cae8960c8e102761e3caacc5759d0fc955a38", + "bytes": 16462, + "media_type": "application/json" + } + }, + "asset_type": "model3d", + "original_filename": "robotic.json", + "original_format": "model3d", + "normalized_format": "model3d", + "provenance": { + "origin": "procedural-fixture", + "fixture": true, + "description": "Self-authored editor binding fixture; missing scene owner is intentional.", + "source_refs": [] + }, + "license": { + "identifier": null, + "attribution": null, + "permission": "unknown", + "evidence": null + }, + "dependencies": [], + "thumbnails": [], + "validation_status": "unvalidated", + "previous": null +} diff --git a/examples/editor3d-contracts/command.json b/examples/editor3d-contracts/command.json new file mode 100644 index 0000000..3a562c8 --- /dev/null +++ b/examples/editor3d-contracts/command.json @@ -0,0 +1,28 @@ +{ + "schema_version": 1, + "kind": "editor3d-command", + "id": "command", + "project_id": "project", + "scene_id": "scene", + "expected_revision": 0, + "expected_project_sha256": "62ca33b39a103db474de2a1e5f285a7bdc95d4d6d90550a2c716ca2826a57562", + "transaction_id": "transaction", + "operation": "set-transform", + "targets": [ + "node" + ], + "input": { + "owner": "scene3d", + "id": "native-scene", + "version": "1.0.0", + "native_kind": "scene3d-scene", + "file": { + "path": "unavailable/scene.json", + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "bytes": 1, + "media_type": "application/json" + } + }, + "inverse": null, + "author": "fixture-author" +} diff --git a/examples/editor3d-contracts/derived.json b/examples/editor3d-contracts/derived.json new file mode 100644 index 0000000..aa2c7f0 --- /dev/null +++ b/examples/editor3d-contracts/derived.json @@ -0,0 +1,27 @@ +{ + "schema_version": 1, + "kind": "editor3d-derived", + "project_id": "project", + "project_sha256": "62ca33b39a103db474de2a1e5f285a7bdc95d4d6d90550a2c716ca2826a57562", + "frame": 0, + "frame_rate": { + "numerator": 24, + "denominator": 1 + }, + "native_dependencies": [ + { + "owner": "scene3d", + "id": "native-scene", + "version": "1.0.0", + "native_kind": "scene3d-scene", + "file": { + "path": "unavailable/scene.json", + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "bytes": 1, + "media_type": "application/json" + } + } + ], + "evidence": [], + "status": "unavailable" +} diff --git a/examples/editor3d-contracts/node.json b/examples/editor3d-contracts/node.json new file mode 100644 index 0000000..da57f16 --- /dev/null +++ b/examples/editor3d-contracts/node.json @@ -0,0 +1,33 @@ +{ + "schema_version": 1, + "kind": "editor3d-node", + "id": "node", + "scene_id": "scene", + "native": { + "owner": "scene3d", + "id": "native-scene", + "version": "1.0.0", + "native_kind": "scene3d-scene", + "file": { + "path": "unavailable/scene.json", + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "bytes": 1, + "media_type": "application/json" + } + }, + "native_node_id": "native-node", + "node_type": "mesh", + "parent": null, + "sibling_order": 0, + "selectable": true, + "locked": false, + "collections": [ + "default" + ], + "asset_id": "asset", + "rig_id": null, + "editor_metadata": { + "label": "Engineering contract fixture", + "notes": "No native scene or renderer execution is claimed." + } +} diff --git a/examples/editor3d-contracts/project.json b/examples/editor3d-contracts/project.json new file mode 100644 index 0000000..bae8996 --- /dev/null +++ b/examples/editor3d-contracts/project.json @@ -0,0 +1,84 @@ +{ + "schema_version": 1, + "kind": "editor3d-project", + "id": "project", + "name": "Editor contract fixture", + "revision": 0, + "created_at": "2026-10-25T00:00:00Z", + "updated_at": "2026-10-25T00:00:00Z", + "application": { + "name": "hatch-editor3d", + "version": "1.0.0" + }, + "coordinates": { + "handedness": "right", + "right": "+X", + "up": "+Y", + "forward": "+Z", + "unit": "meter", + "angles": "radian", + "quaternion": "XYZW", + "matrices": "column-vectors", + "time": "second" + }, + "color_management": null, + "frame_rate": { + "numerator": 24, + "denominator": 1 + }, + "scenes": [ + { + "id": "scene", + "sha256": "3bfcbb1027004a7f7b93f489b7504d5b4f10a53ebd8e721fa36a9c543cc6b21f" + } + ], + "shots": [], + "shared_assets": [ + { + "id": "asset", + "sha256": "01b2ce2dbc1149a770b1cfb383f8545638c839e4ce9190f025aa6fc1f53d3740" + } + ], + "characters": [], + "animation_clips": [], + "render_presets": [], + "editor_metadata": { + "label": "Engineering contract fixture", + "notes": "No native scene or renderer execution is claimed." + }, + "provenance": { + "origin": "procedural-fixture", + "fixture": true, + "description": "Self-authored editor binding fixture; missing scene owner is intentional.", + "source_refs": [] + }, + "dependency_manifest": [ + { + "owner": "model3d", + "id": "robotic-contract-example", + "version": "1.0.0", + "native_kind": "model3d-asset", + "file": { + "path": "examples/model3d-contracts/robotic.json", + "sha256": "ce666b1b75c7b183007886e5674cae8960c8e102761e3caacc5759d0fc955a38", + "bytes": 16462, + "media_type": "application/json" + } + }, + { + "owner": "scene3d", + "id": "native-scene", + "version": "1.0.0", + "native_kind": "scene3d-scene", + "file": { + "path": "unavailable/scene.json", + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "bytes": 1, + "media_type": "application/json" + } + } + ], + "source_projects": [], + "validation_status": "unvalidated", + "review_status": "unreviewed" +} diff --git a/examples/editor3d-contracts/request.json b/examples/editor3d-contracts/request.json new file mode 100644 index 0000000..58765a7 --- /dev/null +++ b/examples/editor3d-contracts/request.json @@ -0,0 +1,17 @@ +{ + "schema_version": 1, + "kind": "editor3d-request", + "id": "request", + "operation": "scenes.command", + "project_id": "project", + "target_id": "scene", + "expected_revision": 0, + "expected_project_sha256": "62ca33b39a103db474de2a1e5f285a7bdc95d4d6d90550a2c716ca2826a57562", + "argument": { + "path": "examples/editor3d-contracts/command.json", + "sha256": "477b45ef3132e99e3246ac4240b7c1f392b20dffcbb245f8fc2d68824ab893e4", + "bytes": 724, + "media_type": "application/json" + }, + "idempotency_key": "operation" +} diff --git a/examples/editor3d-contracts/response.json b/examples/editor3d-contracts/response.json new file mode 100644 index 0000000..86b52ce --- /dev/null +++ b/examples/editor3d-contracts/response.json @@ -0,0 +1,20 @@ +{ + "schema_version": 1, + "kind": "editor3d-response", + "request_id": "request", + "status": "unavailable", + "project_revision": 0, + "project_sha256": "62ca33b39a103db474de2a1e5f285a7bdc95d4d6d90550a2c716ca2826a57562", + "outputs": [], + "diagnostics": [ + { + "code": "EDITOR3D_UNAVAILABLE", + "path": "/operation", + "message": "Scene owner not integrated.", + "correction": "Integrate the authoritative scene service before authoring.", + "retryable": false + } + ], + "executed": false, + "review_status": "unreviewed" +} diff --git a/examples/editor3d-contracts/revision.json b/examples/editor3d-contracts/revision.json new file mode 100644 index 0000000..ea47b79 --- /dev/null +++ b/examples/editor3d-contracts/revision.json @@ -0,0 +1,18 @@ +{ + "schema_version": 1, + "kind": "editor3d-revision", + "id": "revision", + "project_id": "project", + "number": 0, + "parent": null, + "project_sha256": "62ca33b39a103db474de2a1e5f285a7bdc95d4d6d90550a2c716ca2826a57562", + "command_ids": [], + "created_at": "2026-10-25T00:00:00Z", + "author": "fixture-author", + "snapshot": { + "path": "examples/editor3d-contracts/project.json", + "sha256": "2061ed7b6d4e7ea8b652c3a32338ddb4ec9aa36173345009055cf7645b0af3aa", + "bytes": 2157, + "media_type": "application/json" + } +} diff --git a/examples/editor3d-contracts/scene.json b/examples/editor3d-contracts/scene.json new file mode 100644 index 0000000..7fc6c33 --- /dev/null +++ b/examples/editor3d-contracts/scene.json @@ -0,0 +1,37 @@ +{ + "schema_version": 1, + "kind": "editor3d-scene", + "id": "scene", + "project_id": "project", + "native": { + "owner": "scene3d", + "id": "native-scene", + "version": "1.0.0", + "native_kind": "scene3d-scene", + "file": { + "path": "unavailable/scene.json", + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "bytes": 1, + "media_type": "application/json" + } + }, + "nodes": [ + { + "id": "node", + "sha256": "9e247c1d7d9fc7c2bed09eb2453c5ef41e467ee4575abdd17c0b37eeb45eaffa" + } + ], + "collections": [ + "default" + ], + "default_camera": null, + "animations": [], + "timeline": { + "start": 0, + "end": 720 + }, + "editor_metadata": { + "label": "Engineering contract fixture", + "notes": "No native scene or renderer execution is claimed." + } +} diff --git a/examples/editor3d-contracts/session.json b/examples/editor3d-contracts/session.json new file mode 100644 index 0000000..2888a6d --- /dev/null +++ b/examples/editor3d-contracts/session.json @@ -0,0 +1,36 @@ +{ + "schema_version": 1, + "kind": "editor3d-session", + "id": "session", + "project_id": "project", + "base_revision": 0, + "base_project_sha256": "62ca33b39a103db474de2a1e5f285a7bdc95d4d6d90550a2c716ca2826a57562", + "selection": [ + "node" + ], + "active_scene": "scene", + "playhead": 0, + "viewport": { + "translation": [ + 0, + 0, + 5 + ], + "rotation": [ + 0, + 0, + 0, + 1 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + "overlays": [ + "grid" + ], + "tool": "select", + "unsaved_command_ids": [] +} diff --git a/profiles/editor3d.schema.json b/profiles/editor3d.schema.json new file mode 100644 index 0000000..7bd63ed --- /dev/null +++ b/profiles/editor3d.schema.json @@ -0,0 +1,3266 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Editor3D v1 resource bindings and service admission contracts", + "$defs": { + "project": { + "type": "object", + "additionalProperties": false, + "properties": { + "schema_version": { + "const": 1 + }, + "kind": { + "const": "editor3d-project" + }, + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "revision": { + "type": "integer", + "minimum": 0, + "maximum": 2000000000 + }, + "created_at": { + "type": "string", + "format": "date-time", + "maxLength": 64, + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}[Tt](?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](?:\\.[0-9]{1,6})?(?:[Zz]|[+-](?:[01][0-9]|2[0-3]):[0-5][0-9])(?![\\s\\S])" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "maxLength": 64, + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}[Tt](?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](?:\\.[0-9]{1,6})?(?:[Zz]|[+-](?:[01][0-9]|2[0-3]):[0-5][0-9])(?![\\s\\S])" + }, + "application": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "const": "hatch-editor3d" + }, + "version": { + "type": "string", + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?![\\s\\S])" + } + }, + "required": [ + "name", + "version" + ] + }, + "coordinates": { + "const": { + "handedness": "right", + "right": "+X", + "up": "+Y", + "forward": "+Z", + "unit": "meter", + "angles": "radian", + "quaternion": "XYZW", + "matrices": "column-vectors", + "time": "second" + } + }, + "color_management": { + "anyOf": [ + { + "type": "object", + "additionalProperties": false, + "properties": { + "owner": { + "enum": [ + "rig", + "model3d", + "scene3d", + "production2d", + "camera3d", + "lookdev", + "animation3d" + ] + }, + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "version": { + "type": "string", + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?![\\s\\S])" + }, + "native_kind": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "file": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "minLength": 1, + "maxLength": 1024, + "pattern": "^(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .](?:/(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .])*(?![\\s\\S])" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + }, + "bytes": { + "type": "integer", + "minimum": 1, + "maximum": 268435456 + }, + "media_type": { + "enum": [ + "model/gltf-binary", + "model/gltf+json", + "model/obj", + "application/octet-stream", + "application/json", + "image/png", + "image/jpeg", + "image/webp", + "text/plain", + "application/zip" + ] + } + }, + "required": [ + "path", + "sha256", + "bytes", + "media_type" + ] + } + }, + "required": [ + "owner", + "id", + "version", + "native_kind", + "file" + ] + }, + { + "type": "null" + } + ] + }, + "frame_rate": { + "type": "object", + "additionalProperties": false, + "properties": { + "numerator": { + "type": "integer", + "minimum": 1, + "maximum": 240000 + }, + "denominator": { + "type": "integer", + "minimum": 1, + "maximum": 10000 + } + }, + "required": [ + "numerator", + "denominator" + ] + }, + "scenes": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + } + }, + "required": [ + "id", + "sha256" + ] + }, + "minItems": 0, + "maxItems": 4096 + }, + "shots": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "owner": { + "enum": [ + "rig", + "model3d", + "scene3d", + "production2d", + "camera3d", + "lookdev", + "animation3d" + ] + }, + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "version": { + "type": "string", + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?![\\s\\S])" + }, + "native_kind": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "file": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "minLength": 1, + "maxLength": 1024, + "pattern": "^(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .](?:/(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .])*(?![\\s\\S])" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + }, + "bytes": { + "type": "integer", + "minimum": 1, + "maximum": 268435456 + }, + "media_type": { + "enum": [ + "model/gltf-binary", + "model/gltf+json", + "model/obj", + "application/octet-stream", + "application/json", + "image/png", + "image/jpeg", + "image/webp", + "text/plain", + "application/zip" + ] + } + }, + "required": [ + "path", + "sha256", + "bytes", + "media_type" + ] + } + }, + "required": [ + "owner", + "id", + "version", + "native_kind", + "file" + ] + }, + "minItems": 0, + "maxItems": 4096 + }, + "shared_assets": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + } + }, + "required": [ + "id", + "sha256" + ] + }, + "minItems": 0, + "maxItems": 4096 + }, + "characters": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "owner": { + "enum": [ + "rig", + "model3d", + "scene3d", + "production2d", + "camera3d", + "lookdev", + "animation3d" + ] + }, + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "version": { + "type": "string", + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?![\\s\\S])" + }, + "native_kind": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "file": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "minLength": 1, + "maxLength": 1024, + "pattern": "^(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .](?:/(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .])*(?![\\s\\S])" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + }, + "bytes": { + "type": "integer", + "minimum": 1, + "maximum": 268435456 + }, + "media_type": { + "enum": [ + "model/gltf-binary", + "model/gltf+json", + "model/obj", + "application/octet-stream", + "application/json", + "image/png", + "image/jpeg", + "image/webp", + "text/plain", + "application/zip" + ] + } + }, + "required": [ + "path", + "sha256", + "bytes", + "media_type" + ] + } + }, + "required": [ + "owner", + "id", + "version", + "native_kind", + "file" + ] + }, + "minItems": 0, + "maxItems": 4096 + }, + "animation_clips": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + } + }, + "required": [ + "id", + "sha256" + ] + }, + "minItems": 0, + "maxItems": 4096 + }, + "render_presets": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "owner": { + "enum": [ + "rig", + "model3d", + "scene3d", + "production2d", + "camera3d", + "lookdev", + "animation3d" + ] + }, + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "version": { + "type": "string", + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?![\\s\\S])" + }, + "native_kind": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "file": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "minLength": 1, + "maxLength": 1024, + "pattern": "^(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .](?:/(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .])*(?![\\s\\S])" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + }, + "bytes": { + "type": "integer", + "minimum": 1, + "maximum": 268435456 + }, + "media_type": { + "enum": [ + "model/gltf-binary", + "model/gltf+json", + "model/obj", + "application/octet-stream", + "application/json", + "image/png", + "image/jpeg", + "image/webp", + "text/plain", + "application/zip" + ] + } + }, + "required": [ + "path", + "sha256", + "bytes", + "media_type" + ] + } + }, + "required": [ + "owner", + "id", + "version", + "native_kind", + "file" + ] + }, + "minItems": 0, + "maxItems": 128 + }, + "editor_metadata": { + "type": "object", + "additionalProperties": false, + "properties": { + "label": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "notes": { + "type": "string", + "maxLength": 4096 + } + }, + "required": [ + "label", + "notes" + ] + }, + "provenance": { + "type": "object", + "additionalProperties": false, + "properties": { + "origin": { + "enum": [ + "authored", + "imported", + "procedural-fixture" + ] + }, + "fixture": { + "type": "boolean" + }, + "description": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "source_refs": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "minLength": 1, + "maxLength": 1024, + "pattern": "^(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .](?:/(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .])*(?![\\s\\S])" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + }, + "bytes": { + "type": "integer", + "minimum": 1, + "maximum": 268435456 + }, + "media_type": { + "enum": [ + "model/gltf-binary", + "model/gltf+json", + "model/obj", + "application/octet-stream", + "application/json", + "image/png", + "image/jpeg", + "image/webp", + "text/plain", + "application/zip" + ] + } + }, + "required": [ + "path", + "sha256", + "bytes", + "media_type" + ] + }, + "minItems": 0, + "maxItems": 128 + } + }, + "required": [ + "origin", + "fixture", + "description", + "source_refs" + ] + }, + "dependency_manifest": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "owner": { + "enum": [ + "rig", + "model3d", + "scene3d", + "production2d", + "camera3d", + "lookdev", + "animation3d" + ] + }, + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "version": { + "type": "string", + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?![\\s\\S])" + }, + "native_kind": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "file": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "minLength": 1, + "maxLength": 1024, + "pattern": "^(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .](?:/(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .])*(?![\\s\\S])" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + }, + "bytes": { + "type": "integer", + "minimum": 1, + "maximum": 268435456 + }, + "media_type": { + "enum": [ + "model/gltf-binary", + "model/gltf+json", + "model/obj", + "application/octet-stream", + "application/json", + "image/png", + "image/jpeg", + "image/webp", + "text/plain", + "application/zip" + ] + } + }, + "required": [ + "path", + "sha256", + "bytes", + "media_type" + ] + } + }, + "required": [ + "owner", + "id", + "version", + "native_kind", + "file" + ] + }, + "minItems": 0, + "maxItems": 4096 + }, + "source_projects": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "owner": { + "enum": [ + "rig", + "model3d", + "scene3d", + "production2d", + "camera3d", + "lookdev", + "animation3d" + ] + }, + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "version": { + "type": "string", + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?![\\s\\S])" + }, + "native_kind": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "file": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "minLength": 1, + "maxLength": 1024, + "pattern": "^(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .](?:/(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .])*(?![\\s\\S])" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + }, + "bytes": { + "type": "integer", + "minimum": 1, + "maximum": 268435456 + }, + "media_type": { + "enum": [ + "model/gltf-binary", + "model/gltf+json", + "model/obj", + "application/octet-stream", + "application/json", + "image/png", + "image/jpeg", + "image/webp", + "text/plain", + "application/zip" + ] + } + }, + "required": [ + "path", + "sha256", + "bytes", + "media_type" + ] + } + }, + "required": [ + "owner", + "id", + "version", + "native_kind", + "file" + ] + }, + "minItems": 0, + "maxItems": 128 + }, + "validation_status": { + "enum": [ + "unvalidated", + "valid", + "invalid" + ] + }, + "review_status": { + "const": "unreviewed" + } + }, + "required": [ + "schema_version", + "kind", + "id", + "name", + "revision", + "created_at", + "updated_at", + "application", + "coordinates", + "color_management", + "frame_rate", + "scenes", + "shots", + "shared_assets", + "characters", + "animation_clips", + "render_presets", + "editor_metadata", + "provenance", + "dependency_manifest", + "source_projects", + "validation_status", + "review_status" + ] + }, + "scene": { + "type": "object", + "additionalProperties": false, + "properties": { + "schema_version": { + "const": 1 + }, + "kind": { + "const": "editor3d-scene" + }, + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "project_id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "native": { + "type": "object", + "additionalProperties": false, + "properties": { + "owner": { + "enum": [ + "rig", + "model3d", + "scene3d", + "production2d", + "camera3d", + "lookdev", + "animation3d" + ] + }, + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "version": { + "type": "string", + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?![\\s\\S])" + }, + "native_kind": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "file": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "minLength": 1, + "maxLength": 1024, + "pattern": "^(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .](?:/(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .])*(?![\\s\\S])" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + }, + "bytes": { + "type": "integer", + "minimum": 1, + "maximum": 268435456 + }, + "media_type": { + "enum": [ + "model/gltf-binary", + "model/gltf+json", + "model/obj", + "application/octet-stream", + "application/json", + "image/png", + "image/jpeg", + "image/webp", + "text/plain", + "application/zip" + ] + } + }, + "required": [ + "path", + "sha256", + "bytes", + "media_type" + ] + } + }, + "required": [ + "owner", + "id", + "version", + "native_kind", + "file" + ] + }, + "nodes": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + } + }, + "required": [ + "id", + "sha256" + ] + }, + "minItems": 0, + "maxItems": 4096 + }, + "collections": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "minItems": 0, + "maxItems": 4096, + "uniqueItems": true + }, + "default_camera": { + "anyOf": [ + { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + { + "type": "null" + } + ] + }, + "animations": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "minItems": 0, + "maxItems": 4096, + "uniqueItems": true + }, + "timeline": { + "type": "object", + "additionalProperties": false, + "properties": { + "start": { + "type": "integer", + "minimum": 0, + "maximum": 2000000000 + }, + "end": { + "type": "integer", + "minimum": 0, + "maximum": 2000000000 + } + }, + "required": [ + "start", + "end" + ] + }, + "editor_metadata": { + "type": "object", + "additionalProperties": false, + "properties": { + "label": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "notes": { + "type": "string", + "maxLength": 4096 + } + }, + "required": [ + "label", + "notes" + ] + } + }, + "required": [ + "schema_version", + "kind", + "id", + "project_id", + "native", + "nodes", + "collections", + "default_camera", + "animations", + "timeline", + "editor_metadata" + ] + }, + "node": { + "type": "object", + "additionalProperties": false, + "properties": { + "schema_version": { + "const": 1 + }, + "kind": { + "const": "editor3d-node" + }, + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "scene_id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "native": { + "type": "object", + "additionalProperties": false, + "properties": { + "owner": { + "enum": [ + "rig", + "model3d", + "scene3d", + "production2d", + "camera3d", + "lookdev", + "animation3d" + ] + }, + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "version": { + "type": "string", + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?![\\s\\S])" + }, + "native_kind": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "file": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "minLength": 1, + "maxLength": 1024, + "pattern": "^(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .](?:/(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .])*(?![\\s\\S])" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + }, + "bytes": { + "type": "integer", + "minimum": 1, + "maximum": 268435456 + }, + "media_type": { + "enum": [ + "model/gltf-binary", + "model/gltf+json", + "model/obj", + "application/octet-stream", + "application/json", + "image/png", + "image/jpeg", + "image/webp", + "text/plain", + "application/zip" + ] + } + }, + "required": [ + "path", + "sha256", + "bytes", + "media_type" + ] + } + }, + "required": [ + "owner", + "id", + "version", + "native_kind", + "file" + ] + }, + "native_node_id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "node_type": { + "enum": [ + "mesh", + "group", + "character", + "armature", + "bone", + "camera", + "point-light", + "directional-light", + "spot-light", + "area-light", + "world", + "primitive", + "instance" + ] + }, + "parent": { + "anyOf": [ + { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + { + "type": "null" + } + ] + }, + "sibling_order": { + "type": "integer", + "minimum": 0, + "maximum": 2000000000 + }, + "selectable": { + "type": "boolean" + }, + "locked": { + "type": "boolean" + }, + "collections": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "minItems": 0, + "maxItems": 4096, + "uniqueItems": true + }, + "asset_id": { + "anyOf": [ + { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + { + "type": "null" + } + ] + }, + "rig_id": { + "anyOf": [ + { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + { + "type": "null" + } + ] + }, + "editor_metadata": { + "type": "object", + "additionalProperties": false, + "properties": { + "label": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "notes": { + "type": "string", + "maxLength": 4096 + } + }, + "required": [ + "label", + "notes" + ] + } + }, + "required": [ + "schema_version", + "kind", + "id", + "scene_id", + "native", + "native_node_id", + "node_type", + "parent", + "sibling_order", + "selectable", + "locked", + "collections", + "asset_id", + "rig_id", + "editor_metadata" + ] + }, + "asset": { + "type": "object", + "additionalProperties": false, + "properties": { + "schema_version": { + "const": 1 + }, + "kind": { + "const": "editor3d-asset" + }, + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "native": { + "type": "object", + "additionalProperties": false, + "properties": { + "owner": { + "enum": [ + "rig", + "model3d", + "scene3d", + "production2d", + "camera3d", + "lookdev", + "animation3d" + ] + }, + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "version": { + "type": "string", + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?![\\s\\S])" + }, + "native_kind": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "file": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "minLength": 1, + "maxLength": 1024, + "pattern": "^(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .](?:/(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .])*(?![\\s\\S])" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + }, + "bytes": { + "type": "integer", + "minimum": 1, + "maximum": 268435456 + }, + "media_type": { + "enum": [ + "model/gltf-binary", + "model/gltf+json", + "model/obj", + "application/octet-stream", + "application/json", + "image/png", + "image/jpeg", + "image/webp", + "text/plain", + "application/zip" + ] + } + }, + "required": [ + "path", + "sha256", + "bytes", + "media_type" + ] + } + }, + "required": [ + "owner", + "id", + "version", + "native_kind", + "file" + ] + }, + "asset_type": { + "enum": [ + "model3d", + "rig", + "image2d", + "reference2d", + "material", + "texture", + "camera", + "animation" + ] + }, + "original_filename": { + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + "original_format": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "normalized_format": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "provenance": { + "type": "object", + "additionalProperties": false, + "properties": { + "origin": { + "enum": [ + "authored", + "imported", + "procedural-fixture" + ] + }, + "fixture": { + "type": "boolean" + }, + "description": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "source_refs": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "minLength": 1, + "maxLength": 1024, + "pattern": "^(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .](?:/(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .])*(?![\\s\\S])" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + }, + "bytes": { + "type": "integer", + "minimum": 1, + "maximum": 268435456 + }, + "media_type": { + "enum": [ + "model/gltf-binary", + "model/gltf+json", + "model/obj", + "application/octet-stream", + "application/json", + "image/png", + "image/jpeg", + "image/webp", + "text/plain", + "application/zip" + ] + } + }, + "required": [ + "path", + "sha256", + "bytes", + "media_type" + ] + }, + "minItems": 0, + "maxItems": 128 + } + }, + "required": [ + "origin", + "fixture", + "description", + "source_refs" + ] + }, + "license": { + "type": "object", + "additionalProperties": false, + "properties": { + "identifier": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + { + "type": "null" + } + ] + }, + "attribution": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + { + "type": "null" + } + ] + }, + "permission": { + "enum": [ + "unknown", + "restricted", + "permitted" + ] + }, + "evidence": { + "anyOf": [ + { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "minLength": 1, + "maxLength": 1024, + "pattern": "^(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .](?:/(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .])*(?![\\s\\S])" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + }, + "bytes": { + "type": "integer", + "minimum": 1, + "maximum": 268435456 + }, + "media_type": { + "enum": [ + "model/gltf-binary", + "model/gltf+json", + "model/obj", + "application/octet-stream", + "application/json", + "image/png", + "image/jpeg", + "image/webp", + "text/plain", + "application/zip" + ] + } + }, + "required": [ + "path", + "sha256", + "bytes", + "media_type" + ] + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "identifier", + "attribution", + "permission", + "evidence" + ] + }, + "dependencies": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "owner": { + "enum": [ + "rig", + "model3d", + "scene3d", + "production2d", + "camera3d", + "lookdev", + "animation3d" + ] + }, + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "version": { + "type": "string", + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?![\\s\\S])" + }, + "native_kind": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "file": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "minLength": 1, + "maxLength": 1024, + "pattern": "^(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .](?:/(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .])*(?![\\s\\S])" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + }, + "bytes": { + "type": "integer", + "minimum": 1, + "maximum": 268435456 + }, + "media_type": { + "enum": [ + "model/gltf-binary", + "model/gltf+json", + "model/obj", + "application/octet-stream", + "application/json", + "image/png", + "image/jpeg", + "image/webp", + "text/plain", + "application/zip" + ] + } + }, + "required": [ + "path", + "sha256", + "bytes", + "media_type" + ] + } + }, + "required": [ + "owner", + "id", + "version", + "native_kind", + "file" + ] + }, + "minItems": 0, + "maxItems": 4096 + }, + "thumbnails": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "minLength": 1, + "maxLength": 1024, + "pattern": "^(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .](?:/(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .])*(?![\\s\\S])" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + }, + "bytes": { + "type": "integer", + "minimum": 1, + "maximum": 268435456 + }, + "media_type": { + "enum": [ + "model/gltf-binary", + "model/gltf+json", + "model/obj", + "application/octet-stream", + "application/json", + "image/png", + "image/jpeg", + "image/webp", + "text/plain", + "application/zip" + ] + } + }, + "required": [ + "path", + "sha256", + "bytes", + "media_type" + ] + }, + "minItems": 0, + "maxItems": 32 + }, + "validation_status": { + "enum": [ + "unvalidated", + "valid", + "invalid" + ] + }, + "previous": { + "anyOf": [ + { + "type": "object", + "additionalProperties": false, + "properties": { + "owner": { + "enum": [ + "rig", + "model3d", + "scene3d", + "production2d", + "camera3d", + "lookdev", + "animation3d" + ] + }, + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "version": { + "type": "string", + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?![\\s\\S])" + }, + "native_kind": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "file": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "minLength": 1, + "maxLength": 1024, + "pattern": "^(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .](?:/(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .])*(?![\\s\\S])" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + }, + "bytes": { + "type": "integer", + "minimum": 1, + "maximum": 268435456 + }, + "media_type": { + "enum": [ + "model/gltf-binary", + "model/gltf+json", + "model/obj", + "application/octet-stream", + "application/json", + "image/png", + "image/jpeg", + "image/webp", + "text/plain", + "application/zip" + ] + } + }, + "required": [ + "path", + "sha256", + "bytes", + "media_type" + ] + } + }, + "required": [ + "owner", + "id", + "version", + "native_kind", + "file" + ] + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "schema_version", + "kind", + "id", + "native", + "asset_type", + "original_filename", + "original_format", + "normalized_format", + "provenance", + "license", + "dependencies", + "thumbnails", + "validation_status", + "previous" + ] + }, + "animation": { + "type": "object", + "additionalProperties": false, + "properties": { + "schema_version": { + "const": 1 + }, + "kind": { + "const": "editor3d-animation" + }, + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "native": { + "type": "object", + "additionalProperties": false, + "properties": { + "owner": { + "enum": [ + "rig", + "model3d", + "scene3d", + "production2d", + "camera3d", + "lookdev", + "animation3d" + ] + }, + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "version": { + "type": "string", + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?![\\s\\S])" + }, + "native_kind": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "file": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "minLength": 1, + "maxLength": 1024, + "pattern": "^(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .](?:/(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .])*(?![\\s\\S])" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + }, + "bytes": { + "type": "integer", + "minimum": 1, + "maximum": 268435456 + }, + "media_type": { + "enum": [ + "model/gltf-binary", + "model/gltf+json", + "model/obj", + "application/octet-stream", + "application/json", + "image/png", + "image/jpeg", + "image/webp", + "text/plain", + "application/zip" + ] + } + }, + "required": [ + "path", + "sha256", + "bytes", + "media_type" + ] + } + }, + "required": [ + "owner", + "id", + "version", + "native_kind", + "file" + ] + }, + "targets": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "minItems": 0, + "maxItems": 4096, + "uniqueItems": true + }, + "frame_rate": { + "type": "object", + "additionalProperties": false, + "properties": { + "numerator": { + "type": "integer", + "minimum": 1, + "maximum": 240000 + }, + "denominator": { + "type": "integer", + "minimum": 1, + "maximum": 10000 + } + }, + "required": [ + "numerator", + "denominator" + ] + }, + "interval": { + "type": "object", + "additionalProperties": false, + "properties": { + "start": { + "type": "integer", + "minimum": 0, + "maximum": 2000000000 + }, + "end": { + "type": "integer", + "minimum": 0, + "maximum": 2000000000 + } + }, + "required": [ + "start", + "end" + ] + } + }, + "required": [ + "schema_version", + "kind", + "id", + "native", + "targets", + "frame_rate", + "interval" + ] + }, + "command": { + "type": "object", + "additionalProperties": false, + "properties": { + "schema_version": { + "const": 1 + }, + "kind": { + "const": "editor3d-command" + }, + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "project_id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "scene_id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "expected_revision": { + "type": "integer", + "minimum": 0, + "maximum": 2000000000 + }, + "expected_project_sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + }, + "transaction_id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "operation": { + "enum": [ + "set-transform", + "reparent", + "set-metadata", + "create-node", + "delete-node", + "set-native-property", + "replace-animation" + ] + }, + "targets": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "minItems": 0, + "maxItems": 4096, + "uniqueItems": true + }, + "input": { + "type": "object", + "additionalProperties": false, + "properties": { + "owner": { + "enum": [ + "rig", + "model3d", + "scene3d", + "production2d", + "camera3d", + "lookdev", + "animation3d" + ] + }, + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "version": { + "type": "string", + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?![\\s\\S])" + }, + "native_kind": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "file": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "minLength": 1, + "maxLength": 1024, + "pattern": "^(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .](?:/(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .])*(?![\\s\\S])" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + }, + "bytes": { + "type": "integer", + "minimum": 1, + "maximum": 268435456 + }, + "media_type": { + "enum": [ + "model/gltf-binary", + "model/gltf+json", + "model/obj", + "application/octet-stream", + "application/json", + "image/png", + "image/jpeg", + "image/webp", + "text/plain", + "application/zip" + ] + } + }, + "required": [ + "path", + "sha256", + "bytes", + "media_type" + ] + } + }, + "required": [ + "owner", + "id", + "version", + "native_kind", + "file" + ] + }, + "inverse": { + "anyOf": [ + { + "type": "object", + "additionalProperties": false, + "properties": { + "owner": { + "enum": [ + "rig", + "model3d", + "scene3d", + "production2d", + "camera3d", + "lookdev", + "animation3d" + ] + }, + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "version": { + "type": "string", + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?![\\s\\S])" + }, + "native_kind": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "file": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "minLength": 1, + "maxLength": 1024, + "pattern": "^(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .](?:/(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .])*(?![\\s\\S])" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + }, + "bytes": { + "type": "integer", + "minimum": 1, + "maximum": 268435456 + }, + "media_type": { + "enum": [ + "model/gltf-binary", + "model/gltf+json", + "model/obj", + "application/octet-stream", + "application/json", + "image/png", + "image/jpeg", + "image/webp", + "text/plain", + "application/zip" + ] + } + }, + "required": [ + "path", + "sha256", + "bytes", + "media_type" + ] + } + }, + "required": [ + "owner", + "id", + "version", + "native_kind", + "file" + ] + }, + { + "type": "null" + } + ] + }, + "author": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + } + }, + "required": [ + "schema_version", + "kind", + "id", + "project_id", + "scene_id", + "expected_revision", + "expected_project_sha256", + "transaction_id", + "operation", + "targets", + "input", + "inverse", + "author" + ] + }, + "revision": { + "type": "object", + "additionalProperties": false, + "properties": { + "schema_version": { + "const": 1 + }, + "kind": { + "const": "editor3d-revision" + }, + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "project_id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "number": { + "type": "integer", + "minimum": 0, + "maximum": 2000000000 + }, + "parent": { + "anyOf": [ + { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + } + }, + "required": [ + "id", + "sha256" + ] + }, + { + "type": "null" + } + ] + }, + "project_sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + }, + "command_ids": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "minItems": 0, + "maxItems": 4096, + "uniqueItems": true + }, + "created_at": { + "type": "string", + "format": "date-time", + "maxLength": 64, + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}[Tt](?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](?:\\.[0-9]{1,6})?(?:[Zz]|[+-](?:[01][0-9]|2[0-3]):[0-5][0-9])(?![\\s\\S])" + }, + "author": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "snapshot": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "minLength": 1, + "maxLength": 1024, + "pattern": "^(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .](?:/(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .])*(?![\\s\\S])" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + }, + "bytes": { + "type": "integer", + "minimum": 1, + "maximum": 268435456 + }, + "media_type": { + "enum": [ + "model/gltf-binary", + "model/gltf+json", + "model/obj", + "application/octet-stream", + "application/json", + "image/png", + "image/jpeg", + "image/webp", + "text/plain", + "application/zip" + ] + } + }, + "required": [ + "path", + "sha256", + "bytes", + "media_type" + ] + } + }, + "required": [ + "schema_version", + "kind", + "id", + "project_id", + "number", + "parent", + "project_sha256", + "command_ids", + "created_at", + "author", + "snapshot" + ] + }, + "session": { + "type": "object", + "additionalProperties": false, + "properties": { + "schema_version": { + "const": 1 + }, + "kind": { + "const": "editor3d-session" + }, + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "project_id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "base_revision": { + "type": "integer", + "minimum": 0, + "maximum": 2000000000 + }, + "base_project_sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + }, + "selection": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "minItems": 0, + "maxItems": 4096, + "uniqueItems": true + }, + "active_scene": { + "anyOf": [ + { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + { + "type": "null" + } + ] + }, + "playhead": { + "type": "integer", + "minimum": 0, + "maximum": 2000000000 + }, + "viewport": { + "type": "object", + "additionalProperties": false, + "properties": { + "translation": { + "type": "array", + "items": { + "type": "number", + "minimum": -1000000000.0, + "maximum": 1000000000.0 + }, + "minItems": 3, + "maxItems": 3 + }, + "rotation": { + "type": "array", + "items": { + "type": "number", + "minimum": -1000000000.0, + "maximum": 1000000000.0 + }, + "minItems": 4, + "maxItems": 4 + }, + "scale": { + "type": "array", + "items": { + "type": "number", + "exclusiveMinimum": 0, + "maximum": 1000000000.0 + }, + "minItems": 3, + "maxItems": 3 + } + }, + "required": [ + "translation", + "rotation", + "scale" + ] + }, + "overlays": { + "type": "array", + "items": { + "enum": [ + "grid", + "bones", + "bounds", + "selection", + "gizmo" + ] + }, + "minItems": 0, + "maxItems": 5 + }, + "tool": { + "enum": [ + "select", + "translate", + "rotate", + "scale" + ] + }, + "unsaved_command_ids": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "minItems": 0, + "maxItems": 4096, + "uniqueItems": true + } + }, + "required": [ + "schema_version", + "kind", + "id", + "project_id", + "base_revision", + "base_project_sha256", + "selection", + "active_scene", + "playhead", + "viewport", + "overlays", + "tool", + "unsaved_command_ids" + ] + }, + "derived": { + "type": "object", + "additionalProperties": false, + "properties": { + "schema_version": { + "const": 1 + }, + "kind": { + "const": "editor3d-derived" + }, + "project_id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "project_sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + }, + "frame": { + "type": "integer", + "minimum": 0, + "maximum": 2000000000 + }, + "frame_rate": { + "type": "object", + "additionalProperties": false, + "properties": { + "numerator": { + "type": "integer", + "minimum": 1, + "maximum": 240000 + }, + "denominator": { + "type": "integer", + "minimum": 1, + "maximum": 10000 + } + }, + "required": [ + "numerator", + "denominator" + ] + }, + "native_dependencies": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "owner": { + "enum": [ + "rig", + "model3d", + "scene3d", + "production2d", + "camera3d", + "lookdev", + "animation3d" + ] + }, + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "version": { + "type": "string", + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?![\\s\\S])" + }, + "native_kind": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "file": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "minLength": 1, + "maxLength": 1024, + "pattern": "^(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .](?:/(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .])*(?![\\s\\S])" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + }, + "bytes": { + "type": "integer", + "minimum": 1, + "maximum": 268435456 + }, + "media_type": { + "enum": [ + "model/gltf-binary", + "model/gltf+json", + "model/obj", + "application/octet-stream", + "application/json", + "image/png", + "image/jpeg", + "image/webp", + "text/plain", + "application/zip" + ] + } + }, + "required": [ + "path", + "sha256", + "bytes", + "media_type" + ] + } + }, + "required": [ + "owner", + "id", + "version", + "native_kind", + "file" + ] + }, + "minItems": 0, + "maxItems": 4096 + }, + "evidence": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "minLength": 1, + "maxLength": 1024, + "pattern": "^(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .](?:/(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .])*(?![\\s\\S])" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + }, + "bytes": { + "type": "integer", + "minimum": 1, + "maximum": 268435456 + }, + "media_type": { + "enum": [ + "model/gltf-binary", + "model/gltf+json", + "model/obj", + "application/octet-stream", + "application/json", + "image/png", + "image/jpeg", + "image/webp", + "text/plain", + "application/zip" + ] + } + }, + "required": [ + "path", + "sha256", + "bytes", + "media_type" + ] + }, + "minItems": 0, + "maxItems": 4096 + }, + "status": { + "enum": [ + "unavailable", + "stale", + "verified" + ] + } + }, + "required": [ + "schema_version", + "kind", + "project_id", + "project_sha256", + "frame", + "frame_rate", + "native_dependencies", + "evidence", + "status" + ] + }, + "request": { + "type": "object", + "additionalProperties": false, + "properties": { + "schema_version": { + "const": 1 + }, + "kind": { + "const": "editor3d-request" + }, + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "operation": { + "enum": [ + "projects.list", + "projects.open", + "projects.get", + "projects.validate", + "revisions.list", + "scenes.list", + "scenes.get", + "scenes.validate", + "assets.list", + "assets.get", + "assets.content", + "assets.validate", + "assets.dependencies", + "render.capabilities", + "render.status", + "render.progress", + "render.manifest", + "render.output", + "recovery.inspect", + "recovery.list", + "recovery.compare", + "recovery.dependencies", + "projects.create", + "projects.save", + "projects.close", + "revisions.create", + "revisions.restore", + "scenes.create", + "scenes.command", + "scenes.delete", + "assets.import", + "assets.thumbnail", + "assets.relink", + "render.create", + "render.cancel", + "render.retry", + "recovery.recover" + ] + }, + "project_id": { + "anyOf": [ + { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + { + "type": "null" + } + ] + }, + "target_id": { + "anyOf": [ + { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + { + "type": "null" + } + ] + }, + "expected_revision": { + "anyOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 2000000000 + }, + { + "type": "null" + } + ] + }, + "expected_project_sha256": { + "anyOf": [ + { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + }, + { + "type": "null" + } + ] + }, + "argument": { + "anyOf": [ + { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "minLength": 1, + "maxLength": 1024, + "pattern": "^(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .](?:/(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .])*(?![\\s\\S])" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + }, + "bytes": { + "type": "integer", + "minimum": 1, + "maximum": 268435456 + }, + "media_type": { + "enum": [ + "model/gltf-binary", + "model/gltf+json", + "model/obj", + "application/octet-stream", + "application/json", + "image/png", + "image/jpeg", + "image/webp", + "text/plain", + "application/zip" + ] + } + }, + "required": [ + "path", + "sha256", + "bytes", + "media_type" + ] + }, + { + "type": "null" + } + ] + }, + "idempotency_key": { + "anyOf": [ + { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "schema_version", + "kind", + "id", + "operation", + "project_id", + "target_id", + "expected_revision", + "expected_project_sha256", + "argument", + "idempotency_key" + ] + }, + "response": { + "type": "object", + "additionalProperties": false, + "properties": { + "schema_version": { + "const": 1 + }, + "kind": { + "const": "editor3d-response" + }, + "request_id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "status": { + "enum": [ + "accepted", + "complete", + "rejected", + "unavailable", + "cancelled" + ] + }, + "project_revision": { + "anyOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 2000000000 + }, + { + "type": "null" + } + ] + }, + "project_sha256": { + "anyOf": [ + { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + }, + { + "type": "null" + } + ] + }, + "outputs": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "minLength": 1, + "maxLength": 1024, + "pattern": "^(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .](?:/(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .])*(?![\\s\\S])" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + }, + "bytes": { + "type": "integer", + "minimum": 1, + "maximum": 268435456 + }, + "media_type": { + "enum": [ + "model/gltf-binary", + "model/gltf+json", + "model/obj", + "application/octet-stream", + "application/json", + "image/png", + "image/jpeg", + "image/webp", + "text/plain", + "application/zip" + ] + } + }, + "required": [ + "path", + "sha256", + "bytes", + "media_type" + ] + }, + "minItems": 0, + "maxItems": 4096 + }, + "diagnostics": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "code": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "path": { + "type": "string", + "maxLength": 1024 + }, + "message": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "correction": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "retryable": { + "type": "boolean" + } + }, + "required": [ + "code", + "path", + "message", + "correction", + "retryable" + ] + }, + "minItems": 0, + "maxItems": 128 + }, + "executed": { + "type": "boolean" + }, + "review_status": { + "const": "unreviewed" + } + }, + "required": [ + "schema_version", + "kind", + "request_id", + "status", + "project_revision", + "project_sha256", + "outputs", + "diagnostics", + "executed", + "review_status" + ] + }, + "NativeReference": { + "type": "object", + "additionalProperties": false, + "properties": { + "owner": { + "enum": [ + "rig", + "model3d", + "scene3d", + "production2d", + "camera3d", + "lookdev", + "animation3d" + ] + }, + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "version": { + "type": "string", + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?![\\s\\S])" + }, + "native_kind": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "file": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "minLength": 1, + "maxLength": 1024, + "pattern": "^(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .](?:/(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .])*(?![\\s\\S])" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + }, + "bytes": { + "type": "integer", + "minimum": 1, + "maximum": 268435456 + }, + "media_type": { + "enum": [ + "model/gltf-binary", + "model/gltf+json", + "model/obj", + "application/octet-stream", + "application/json", + "image/png", + "image/jpeg", + "image/webp", + "text/plain", + "application/zip" + ] + } + }, + "required": [ + "path", + "sha256", + "bytes", + "media_type" + ] + } + }, + "required": [ + "owner", + "id", + "version", + "native_kind", + "file" + ] + }, + "FileReference": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "minLength": 1, + "maxLength": 1024, + "pattern": "^(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .](?:/(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .])*(?![\\s\\S])" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + }, + "bytes": { + "type": "integer", + "minimum": 1, + "maximum": 268435456 + }, + "media_type": { + "enum": [ + "model/gltf-binary", + "model/gltf+json", + "model/obj", + "application/octet-stream", + "application/json", + "image/png", + "image/jpeg", + "image/webp", + "text/plain", + "application/zip" + ] + } + }, + "required": [ + "path", + "sha256", + "bytes", + "media_type" + ] + }, + "ResourcePointer": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + } + }, + "required": [ + "id", + "sha256" + ] + }, + "FrameRate": { + "type": "object", + "additionalProperties": false, + "properties": { + "numerator": { + "type": "integer", + "minimum": 1, + "maximum": 240000 + }, + "denominator": { + "type": "integer", + "minimum": 1, + "maximum": 10000 + } + }, + "required": [ + "numerator", + "denominator" + ] + }, + "FrameInterval": { + "type": "object", + "additionalProperties": false, + "properties": { + "start": { + "type": "integer", + "minimum": 0, + "maximum": 2000000000 + }, + "end": { + "type": "integer", + "minimum": 0, + "maximum": 2000000000 + } + }, + "required": [ + "start", + "end" + ] + }, + "EditorMetadata": { + "type": "object", + "additionalProperties": false, + "properties": { + "label": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "notes": { + "type": "string", + "maxLength": 4096 + } + }, + "required": [ + "label", + "notes" + ] + }, + "Diagnostic": { + "type": "object", + "additionalProperties": false, + "properties": { + "code": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\\s\\S])" + }, + "path": { + "type": "string", + "maxLength": 1024 + }, + "message": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "correction": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "retryable": { + "type": "boolean" + } + }, + "required": [ + "code", + "path", + "message", + "correction", + "retryable" + ] + }, + "Provenance": { + "type": "object", + "additionalProperties": false, + "properties": { + "origin": { + "enum": [ + "authored", + "imported", + "procedural-fixture" + ] + }, + "fixture": { + "type": "boolean" + }, + "description": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "source_refs": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "minLength": 1, + "maxLength": 1024, + "pattern": "^(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .](?:/(?!(?:[aA][uU][xX]|[cC][oO][mM]1|[cC][oO][mM]2|[cC][oO][mM]3|[cC][oO][mM]4|[cC][oO][mM]5|[cC][oO][mM]6|[cC][oO][mM]7|[cC][oO][mM]8|[cC][oO][mM]9|[cC][oO][nN]|[cC][oO][nN][iI][nN]\\$|[cC][oO][nN][oO][uU][tT]\\$|[lL][pP][tT]1|[lL][pP][tT]2|[lL][pP][tT]3|[lL][pP][tT]4|[lL][pP][tT]5|[lL][pP][tT]6|[lL][pP][tT]7|[lL][pP][tT]8|[lL][pP][tT]9|[nN][uU][lL]|[pP][rR][nN])(?:[./]|(?![\\s\\S])))[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff]*[^/\\\\:<>\"|?*\\x00-\\x1f\\x7f\\ud800-\\udfff .])*(?![\\s\\S])" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}(?![\\s\\S])" + }, + "bytes": { + "type": "integer", + "minimum": 1, + "maximum": 268435456 + }, + "media_type": { + "enum": [ + "model/gltf-binary", + "model/gltf+json", + "model/obj", + "application/octet-stream", + "application/json", + "image/png", + "image/jpeg", + "image/webp", + "text/plain", + "application/zip" + ] + } + }, + "required": [ + "path", + "sha256", + "bytes", + "media_type" + ] + }, + "minItems": 0, + "maxItems": 128 + } + }, + "required": [ + "origin", + "fixture", + "description", + "source_refs" + ] + }, + "Transform": { + "type": "object", + "additionalProperties": false, + "properties": { + "translation": { + "type": "array", + "items": { + "type": "number", + "minimum": -1000000000.0, + "maximum": 1000000000.0 + }, + "minItems": 3, + "maxItems": 3 + }, + "rotation": { + "type": "array", + "items": { + "type": "number", + "minimum": -1000000000.0, + "maximum": 1000000000.0 + }, + "minItems": 4, + "maxItems": 4 + }, + "scale": { + "type": "array", + "items": { + "type": "number", + "exclusiveMinimum": 0, + "maximum": 1000000000.0 + }, + "minItems": 3, + "maxItems": 3 + } + }, + "required": [ + "translation", + "rotation", + "scale" + ] + } + }, + "oneOf": [ + { + "$ref": "#/$defs/project" + }, + { + "$ref": "#/$defs/scene" + }, + { + "$ref": "#/$defs/node" + }, + { + "$ref": "#/$defs/asset" + }, + { + "$ref": "#/$defs/animation" + }, + { + "$ref": "#/$defs/command" + }, + { + "$ref": "#/$defs/revision" + }, + { + "$ref": "#/$defs/session" + }, + { + "$ref": "#/$defs/derived" + }, + { + "$ref": "#/$defs/request" + }, + { + "$ref": "#/$defs/response" + } + ] +} diff --git a/scripts/editor3d_adapters.py b/scripts/editor3d_adapters.py new file mode 100644 index 0000000..f2850b6 --- /dev/null +++ b/scripts/editor3d_adapters.py @@ -0,0 +1,176 @@ +"""Small executable editor admission adapters; no server or scene controller. + +Native owners keep payload meaning. Mathematical adapters lazily reuse rig math. +""" +from __future__ import annotations +from fractions import Fraction +import hashlib +import hmac +import math +import os +from pathlib import Path +import stat +from urllib.parse import urlsplit + +from editor3d_contracts import (MAX_DOCUMENT_BYTES, WRITE_OPERATIONS, _definitions, + validate_contract, _error) +from rig_errors import decode_json, require, safe_path + + +def frame_time(frame, fps): + """Exact seconds from a nonnegative frame boundary and reduced frame rate.""" + if (isinstance(frame, bool) or not isinstance(frame, (int, float)) + or isinstance(frame, float) and (not math.isfinite(frame) or not frame.is_integer()) + or not 0 <= frame <= 2_000_000_000): + _error('FRAME', 'Frame boundary must be an integer in the supported range.') + frame = int(frame) + from jsonschema import Draft202012Validator + if not Draft202012Validator(_definitions()['FrameRate']).is_valid(fps): + _error('RATE', 'Use a bounded positive rational frame rate.') + numerator, denominator = int(fps['numerator']), int(fps['denominator']) + if math.gcd(numerator, denominator) != 1: + _error('RATE', 'Frame rate must be reduced.') + return Fraction(frame * denominator, numerator) + + +def transform_to_blender(transform): + """Object transform basis conjugation; camera-local conventions are separate.""" + from jsonschema import Draft202012Validator + if not Draft202012Validator(_definitions()['Transform']).is_valid(transform): + _error('TRANSFORM', 'Use the native rig transform contract.') + if abs(math.hypot(*transform['rotation']) - 1) > 1e-6: + _error('QUATERNION', 'Rotation must be a unit XYZW quaternion.') + from rig_math import BLENDER_FROM_CANONICAL as basis, transform_matrix + matrix = transform_matrix(transform) + return (basis @ matrix @ basis.T).tolist() + + +def euler_to_quaternion(euler): + """Fixed-axis extrinsic XYZ radians: Rz @ Ry @ Rx, normalized XYZW.""" + from rig_math import axis_angle, qmul, quaternion, vector + x, y, z = vector(euler) + return quaternion(qmul(axis_angle([0, 0, 1], float(z)), + qmul(axis_angle([0, 1, 0], float(y)), axis_angle([1, 0, 0], float(x))))).tolist() + + +def quaternion_to_euler(value): + """Return extrinsic XYZ plus gimbal warning; quaternion stays authoritative.""" + from rig_math import qmatrix, quaternion + matrix = qmatrix(quaternion(value, strict=True)) + pitch = math.asin(max(-1., min(1., -float(matrix[2, 0])))) + locked = abs(math.cos(pitch)) < 1e-7 + if locked: + roll = math.atan2(-float(matrix[1, 2]), float(matrix[1, 1])) + yaw = 0. + else: + roll = math.atan2(float(matrix[2, 1]), float(matrix[2, 2])) + yaw = math.atan2(float(matrix[1, 0]), float(matrix[0, 0])) + return {'euler': [roll, pitch, yaw], 'gimbal_lock': locked, + 'convention': 'extrinsic-XYZ-radians'} + + +def verify_native_reference(reference, root): + """Read a bounded regular JSON manifest and invoke its current native owner. + +This verifies a manifest contract, not its transitive resources or a whole scene. +An unmerged owner is unavailable, never silently parsed as a different format. +""" + from jsonschema import Draft202012Validator + if not Draft202012Validator({'$defs': _definitions(), '$ref': '#/$defs/NativeReference'}).is_valid(reference): + _error('REFERENCE', 'Invalid native resource reference.') + owner = reference['owner'] + if owner not in ('rig', 'model3d', 'production2d'): + _error('UNAVAILABLE', 'Native owner is not integrated in this checkout: ' + owner) + declared = reference['file'] + if declared['media_type'] != 'application/json' or declared['bytes'] > MAX_DOCUMENT_BYTES: + _error('REFERENCE', 'Native manifests must be bounded JSON documents.') + path = safe_path(root, declared['path'], exists=True) + fd = os.open(path, os.O_RDONLY | getattr(os, 'O_NOFOLLOW', 0) | getattr(os, 'O_NONBLOCK', 0)) + with os.fdopen(fd, 'rb') as stream: + observed = os.fstat(stream.fileno()) + if not stat.S_ISREG(observed.st_mode) or observed.st_size != declared['bytes']: + _error('BYTES', 'Native manifest byte count differs.') + payload = stream.read(MAX_DOCUMENT_BYTES + 1) + if len(payload) != declared['bytes'] or hashlib.sha256(payload).hexdigest() != declared['sha256']: + _error('HASH', 'Native manifest bytes changed.') + value = decode_json(payload) + if not isinstance(value, dict): + _error('KIND', 'Native manifest must be a JSON object.') + kind = reference['native_kind'] + if owner == 'rig': + from rig_contracts import KINDS, validate_contract as native_validate + if kind not in KINDS.values(): + _error('KIND', 'Unknown rig native kind.') + native_validate(value, kind) + elif owner == 'model3d': + from model3d_contracts import validate_contract as native_validate + if value.get('kind') != kind: + _error('KIND', 'Native Model3D kind differs.') + native_validate(value) + else: + from production_contracts import validate_structure + if kind not in ('project', 'scene', 'shot', 'cast', 'clip'): + _error('KIND', 'Unknown production2d native kind.') + validate_structure(value, kind) + native_id = value.get('id', value.get('project_id')) + if native_id != reference['id']: + _error('IDENTITY', 'Native ID differs from the bound identity.') + native_version = value.get('version', str(value.get('schema_version')) + '.0.0') + if native_version != reference['version']: + _error('VERSION', 'Native version differs from the exact reference.') + return {'status': 'valid-native-contract', 'owner': owner, 'id': native_id, + 'sha256': declared['sha256'], 'transitive_resources_verified': False, + 'review_status': 'unreviewed'} + + +def validate_api_request(request, *, authorization, token, origin, server_origin, + content_type='application/json', content_length=None, + current_revision=None, current_project_sha256=None): + """Authenticate and admit raw JSON request bytes against a saved snapshot. + +The actual service must read no more than Content-Length, reject duplicate HTTP +headers, serialize writes with the native lock, then recheck this snapshot under +that lock. This preflight performs no operation and returns no token or secrets. +""" + if not isinstance(token, str) or len(token) < 24: + _error('TOKEN', 'Server token must contain at least 24 characters.') + try: + authorized = isinstance(authorization, str) and hmac.compare_digest( + authorization.encode('utf-8'), ('Bearer ' + token).encode('utf-8')) + except UnicodeError: + authorized = False + if not authorized: + _error('AUTH', 'A valid local bearer token is required.') + try: + server = urlsplit(server_origin) + except (ValueError, TypeError): + _error('ORIGIN', 'Invalid loopback origin.') + if (server.scheme != 'http' or server.hostname not in ('127.0.0.1', 'localhost') + or server.username or server.password or server.path or server.query or server.fragment): + _error('ORIGIN', 'Configure an exact HTTP loopback origin.') + try: + if server.port is None or not 1 <= server.port <= 65535: + _error('ORIGIN', 'The loopback origin requires an explicit port.') + except ValueError: + _error('ORIGIN', 'Invalid loopback port.') + if origin is not None and origin != server_origin: + _error('ORIGIN', 'Cross-origin requests are refused.') + if not isinstance(content_type, str) or content_type.lower().split(';', 1)[0].strip() != 'application/json': + _error('CONTENT_TYPE', 'Use application/json.') + if isinstance(request, str): + request = request.encode('utf-8') + if not isinstance(request, bytes): + _error('BODY', 'Admission requires the original UTF-8 JSON bytes.') + if (isinstance(content_length, bool) or not isinstance(content_length, int) + or not 0 < content_length <= MAX_DOCUMENT_BYTES or len(request) != content_length): + _error('BODY', 'Require an exact Content-Length of at most 4 MiB.') + value = decode_json(request) + validate_contract(value) + if value['kind'] != 'editor3d-request': + _error('KIND', 'Expected an editor request.') + if value['operation'] in WRITE_OPERATIONS and value['operation'] != 'projects.create': + if current_revision is None or current_project_sha256 is None: + _error('SNAPSHOT', 'A current saved snapshot is required for a mutation.') + if value['expected_revision'] != current_revision or value['expected_project_sha256'] != current_project_sha256: + _error('STALE', 'Reload the saved revision and reapply the command.') + return {'status': 'admitted', 'request': value, 'executed': False} diff --git a/scripts/editor3d_contracts.py b/scripts/editor3d_contracts.py new file mode 100644 index 0000000..1532c30 --- /dev/null +++ b/scripts/editor3d_contracts.py @@ -0,0 +1,504 @@ +"""Editor v1 bindings to native resource owners, never a second scene format. + +These are admission contracts, not implementations of persistence, authoring or +rendering. Importing this module loads no optional numerical dependency. +""" +from __future__ import annotations +from copy import deepcopy +from datetime import datetime +from functools import lru_cache +import argparse +import json +import math +import os +import re +import stat +from pathlib import Path + +from rig_contracts import COORDINATES, obj, arr, ref +from rig_errors import RigError, decode_json, finite_json, object_hash, portable_name, require + +VERSION = '1.0.0' +MAX_DOCUMENT_BYTES = 4 * 1024 * 1024 +KINDS = ('project', 'scene', 'node', 'asset', 'animation', 'command', 'revision', + 'session', 'derived', 'request', 'response') +# The server implementing these contracts remains a separate work package. +READ_OPERATIONS = ('projects.list', 'projects.open', 'projects.get', 'projects.validate', + 'revisions.list', 'scenes.list', 'scenes.get', 'scenes.validate', 'assets.list', + 'assets.get', 'assets.content', 'assets.validate', 'assets.dependencies', + 'render.capabilities', 'render.status', 'render.progress', 'render.manifest', + 'render.output', 'recovery.inspect', 'recovery.list', 'recovery.compare', + 'recovery.dependencies') +WRITE_OPERATIONS = ('projects.create', 'projects.save', 'projects.close', 'revisions.create', + 'revisions.restore', 'scenes.create', 'scenes.command', 'scenes.delete', + 'assets.import', 'assets.thumbnail', 'assets.relink', 'render.create', + 'render.cancel', 'render.retry', 'recovery.recover') +OPERATIONS = READ_OPERATIONS + WRITE_OPERATIONS + +TIMESTAMP_PATTERN = (r'^[0-9]{4}-[0-9]{2}-[0-9]{2}[Tt](?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]' + r'(?:\.[0-9]{1,6})?(?:[Zz]|[+-](?:[01][0-9]|2[0-3]):[0-5][0-9])(?![\s\S])') + + +def _timestamp(value, path): + """Validate the supported offset-aware RFC3339 subset without optional extras.""" + if not isinstance(value, str) or re.fullmatch(TIMESTAMP_PATTERN, value) is None: + _error('TIMESTAMP', 'Use a complete RFC3339 timestamp with an explicit UTC offset.', path) + try: + parsed = datetime.fromisoformat(value.replace('t', 'T').replace('z', 'Z').replace('Z', '+00:00')) + except ValueError as exc: + raise RigError('EDITOR3D_TIMESTAMP', 'Invalid timestamp: ' + str(exc), + stage='editor3d', asset=path) from exc + if parsed.tzinfo is None or parsed.utcoffset() is None: + _error('TIMESTAMP', 'Timestamp must include a UTC offset.', path) + return parsed + + +def _doc(kind, properties): + return obj(dict(schema_version={'const': 1}, kind={'const': 'editor3d-' + kind}, **properties)) + + +@lru_cache(maxsize=1) +def _definitions(): + from model3d_contracts import _definitions as model_definitions + text = dict(type='string', minLength=1, maxLength=4096) + ident = dict(type='string', pattern=r'^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(?![\s\S])') + integer = dict(type='integer', minimum=0, maximum=2_000_000_000) + digest = dict(type='string', pattern=r'^[0-9a-f]{64}(?![\s\S])') + version = dict(type='string', pattern=r'^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?![\s\S])') + nullable = lambda value: {'anyOf': [value, {'type': 'null'}]} + ids = dict(arr(ident, 0, 4096), uniqueItems=True) + stamp = dict(type='string', format='date-time', maxLength=64, pattern=TIMESTAMP_PATTERN) + file = deepcopy(model_definitions()['file']) + native = obj(dict(owner={'enum': ['rig', 'model3d', 'scene3d', 'production2d', + 'camera3d', 'lookdev', 'animation3d']}, id=ident, version=version, + native_kind=ident, file=file)) + pointer = obj(dict(id=ident, sha256=digest)) + fps = obj(dict(numerator=dict(type='integer', minimum=1, maximum=240000), + denominator=dict(type='integer', minimum=1, maximum=10000))) + interval = obj(dict(start=integer, end=integer)) + metadata = obj(dict(label=text, notes=dict(type='string', maxLength=4096))) + diagnostic = obj(dict(code=ident, path=dict(type='string', maxLength=1024), + message=text, correction=text, retryable={'type': 'boolean'})) + provenance = obj(dict(origin={'enum': ['authored', 'imported', 'procedural-fixture']}, + fixture={'type': 'boolean'}, description=text, source_refs=arr(file, 0, 128))) + project = _doc('project', dict(id=ident, name=text, revision=integer, + created_at=stamp, updated_at=stamp, + application=obj(dict(name={'const': 'hatch-editor3d'}, version=version)), + coordinates={'const': COORDINATES}, color_management=nullable(native), + frame_rate=fps, scenes=arr(pointer, 0, 4096), shots=arr(native, 0, 4096), + shared_assets=arr(pointer, 0, 4096), characters=arr(native, 0, 4096), + animation_clips=arr(pointer, 0, 4096), render_presets=arr(native, 0, 128), + editor_metadata=metadata, provenance=provenance, + dependency_manifest=arr(native, 0, 4096), source_projects=arr(native, 0, 128), + validation_status={'enum': ['unvalidated', 'valid', 'invalid']}, + review_status={'const': 'unreviewed'})) + scene = _doc('scene', dict(id=ident, project_id=ident, native=native, + nodes=arr(pointer, 0, 4096), collections=ids, default_camera=nullable(ident), + animations=ids, timeline=interval, editor_metadata=metadata)) + node = _doc('node', dict(id=ident, scene_id=ident, native=native, native_node_id=ident, + node_type={'enum': ['mesh', 'group', 'character', 'armature', 'bone', 'camera', + 'point-light', 'directional-light', 'spot-light', 'area-light', + 'world', 'primitive', 'instance']}, parent=nullable(ident), sibling_order=integer, + selectable={'type': 'boolean'}, locked={'type': 'boolean'}, collections=ids, + asset_id=nullable(ident), rig_id=nullable(ident), editor_metadata=metadata)) + asset = _doc('asset', dict(id=ident, native=native, asset_type={'enum': [ + 'model3d', 'rig', 'image2d', 'reference2d', 'material', 'texture', 'camera', 'animation']}, + original_filename=dict(type='string', minLength=1, maxLength=255), + original_format=ident, normalized_format=ident, provenance=provenance, + license=obj(dict(identifier=nullable(text), attribution=nullable(text), + permission={'enum': ['unknown', 'restricted', 'permitted']}, evidence=nullable(file))), + dependencies=arr(native, 0, 4096), thumbnails=arr(file, 0, 32), + validation_status={'enum': ['unvalidated', 'valid', 'invalid']}, + previous=nullable(native))) + animation = _doc('animation', dict(id=ident, native=native, + targets=ids, frame_rate=fps, interval=interval)) + from rig_contracts import definitions as rig_definitions + transform = deepcopy(rig_definitions()['Bone']['properties']['rest']) + command = _doc('command', dict(id=ident, project_id=ident, scene_id=ident, + expected_revision=integer, expected_project_sha256=digest, + transaction_id=ident, operation={'enum': ['set-transform', 'reparent', 'set-metadata', + 'create-node', 'delete-node', 'set-native-property', 'replace-animation']}, + targets=ids, input=native, inverse=nullable(native), author=ident)) + revision = _doc('revision', dict(id=ident, project_id=ident, number=integer, + parent=nullable(pointer), project_sha256=digest, command_ids=ids, + created_at=stamp, author=ident, snapshot=file)) + session = _doc('session', dict(id=ident, project_id=ident, base_revision=integer, + base_project_sha256=digest, selection=ids, active_scene=nullable(ident), + playhead=integer, viewport=transform, overlays=arr({'enum': ['grid', 'bones', + 'bounds', 'selection', 'gizmo']}, 0, 5), tool={'enum': ['select', 'translate', + 'rotate', 'scale']}, unsaved_command_ids=ids)) + derived = _doc('derived', dict(project_id=ident, project_sha256=digest, + frame=integer, frame_rate=fps, native_dependencies=arr(native, 0, 4096), + evidence=arr(file, 0, 4096), status={'enum': ['unavailable', 'stale', 'verified']})) + request = _doc('request', dict(id=ident, operation={'enum': list(OPERATIONS)}, + project_id=nullable(ident), target_id=nullable(ident), expected_revision=nullable(integer), + expected_project_sha256=nullable(digest), argument=nullable(file), + idempotency_key=nullable(ident))) + response = _doc('response', dict(request_id=ident, + status={'enum': ['accepted', 'complete', 'rejected', 'unavailable', 'cancelled']}, + project_revision=nullable(integer), project_sha256=nullable(digest), + outputs=arr(file, 0, 4096), diagnostics=arr(diagnostic, 0, 128), + executed={'type': 'boolean'}, review_status={'const': 'unreviewed'})) + return dict(project=project, scene=scene, node=node, asset=asset, animation=animation, + command=command, revision=revision, session=session, derived=derived, request=request, + response=response, NativeReference=native, FileReference=file, ResourcePointer=pointer, + FrameRate=fps, FrameInterval=interval, EditorMetadata=metadata, Diagnostic=diagnostic, + Provenance=provenance, Transform=transform) + + +def schemas(): + return {'editor3d.schema.json': {'$schema': 'https://json-schema.org/draft/2020-12/schema', + 'title': 'Editor3D v1 resource bindings and service admission contracts', + '$defs': deepcopy(_definitions()), 'oneOf': [ref(k) for k in KINDS]}} + + +def _error(code, message, path=''): + raise RigError('EDITOR3D_' + code, message, stage='editor3d', asset=path) + + +def _unique(items, label): + values = [i['id'] for i in items] + if len(values) != len(set(values)): + _error('DUPLICATE', 'Resource IDs must be unique.', label) + return set(values) + + +def _walk(value, path=''): + if isinstance(value, dict): + yield value, path + for key, child in value.items(): + yield from _walk(child, path + '/' + key) + elif isinstance(value, list): + for i, child in enumerate(value): + yield from _walk(child, path + '/' + str(i)) + + +def validate_contract(value): + """Validate structure and local semantics; no file is read or state mutated.""" + finite_json(value) + if not isinstance(value, dict) or value.get('kind') not in ['editor3d-' + k for k in KINDS]: + _error('KIND', 'Unsupported editor contract kind.') + from jsonschema import Draft202012Validator, FormatChecker + kind = value['kind'].removeprefix('editor3d-') + validator = Draft202012Validator({'$defs': _definitions(), '$ref': '#/$defs/' + kind}, + format_checker=FormatChecker()) + error = next(validator.iter_errors(value), None) + if error: + _error('SCHEMA', error.message, '/' + '/'.join(map(str, error.absolute_path))) + for child, path in _walk(value): + for field in ('created_at', 'updated_at'): + if field in child: + _timestamp(child[field], path + '/' + field) + if set(child) == {'path', 'sha256', 'bytes', 'media_type'}: + try: + portable_name(child['path']) + except RigError as exc: + _error('PATH', str(exc), path + '/path') + if set(child) == {'numerator', 'denominator'}: + if math.gcd(int(child['numerator']), int(child['denominator'])) != 1: + _error('RATE', 'Use a reduced positive rational frame rate.', path) + if set(child) == {'start', 'end'} and child['start'] >= child['end']: + _error('INTERVAL', 'Frame intervals are nonempty and half-open [start,end).', path) + if set(child) == {'translation', 'rotation', 'scale'}: + if abs(math.hypot(*child['rotation']) - 1) > 1e-6: + _error('QUATERNION', 'Use a normalized XYZW quaternion.', path + '/rotation') + if kind == 'project': + for key in ('scenes', 'shared_assets', 'animation_clips'): + _unique(value[key], '/' + key) + if _timestamp(value['updated_at'], '/updated_at') < _timestamp(value['created_at'], '/created_at'): + _error('TIME', 'updated_at cannot precede created_at.', '/updated_at') + if value['provenance']['origin'] == 'procedural-fixture' and not value['provenance']['fixture']: + _error('PROVENANCE', 'Procedural fixtures must remain explicitly labeled.', '/provenance') + if kind == 'asset': + if '/' in value['original_filename'] or '\\' in value['original_filename']: + _error('PATH', 'Original filename is a basename, not an import path.', '/original_filename') + if value['provenance']['origin'] == 'procedural-fixture' and not value['provenance']['fixture']: + _error('PROVENANCE', 'Procedural fixtures must remain labeled.', '/provenance') + if value['asset_type'] == 'rig' and (value['native']['owner'] != 'rig' + or value['native']['native_kind'] != 'Rig'): + _error('OWNER', 'Rig assets require a native Rig contract.', '/native') + if value['asset_type'] in ('image2d', 'reference2d') and value['native']['owner'] != 'production2d': + _error('OWNER', '2D assets retain production2d authority.', '/native/owner') + if kind == 'command' and not value['targets']: + _error('TARGET', 'A command must name its targets.', '/targets') + if kind == 'node' and value['node_type'] == 'bone' and value['rig_id'] is None: + _error('RIG', 'Bone bindings require an explicit rig owner.', '/rig_id') + if kind == 'scene': + _unique(value['nodes'], '/nodes') + if value['native']['owner'] != 'scene3d': + _error('OWNER', 'Editor scenes bind the scene3d owner.', '/native/owner') + if kind == 'request': + operation = value['operation'] + global_operations = {'projects.list', 'projects.create', 'render.capabilities', 'recovery.list'} + argument_operations = {'projects.create', 'projects.save', 'scenes.create', 'scenes.command', + 'assets.import', 'assets.relink', 'render.create', 'render.retry', 'recovery.recover'} + untargeted = global_operations | {'projects.open', 'projects.get', 'projects.validate', + 'projects.save', 'projects.close', 'revisions.create', 'revisions.list', 'scenes.list', + 'scenes.create', 'assets.list', 'assets.import', 'render.create', 'recovery.inspect', + 'recovery.dependencies'} + if (operation in global_operations) != (value['project_id'] is None): + _error('TARGET', 'This operation has the wrong project scope.', '/project_id') + if (operation in untargeted) != (value['target_id'] is None): + _error('TARGET', 'This operation has the wrong resource scope.', '/target_id') + if (operation in argument_operations) != (value['argument'] is not None): + _error('ARGUMENT', 'This operation has the wrong argument presence.', '/argument') + if operation in READ_OPERATIONS and (value['idempotency_key'] is not None + or value['expected_revision'] is not None or value['expected_project_sha256'] is not None): + _error('PRECONDITION', 'Read operations do not carry mutation preconditions.') + if value['operation'] in WRITE_OPERATIONS: + if value['idempotency_key'] is None: + _error('IDEMPOTENCY', 'Mutations require an idempotency key.', '/idempotency_key') + if value['operation'] != 'projects.create' and any(value[k] is None for k in ('project_id', 'expected_revision', 'expected_project_sha256')): + _error('PRECONDITION', 'Mutations require an exact project revision and digest.') + if value['operation'] == 'projects.create' and any(value[k] is not None for k in ('expected_revision', 'expected_project_sha256')): + _error('PRECONDITION', 'Project creation cannot overwrite an existing revision.') + if kind == 'response': + if value['status'] in ('rejected', 'unavailable') and not value['diagnostics']: + _error('DIAGNOSTIC', 'Failed or unavailable requests need an actionable diagnostic.') + if value['status'] != 'complete' and value['executed']: + _error('STATUS', 'Only complete responses may declare execution.') + if value['status'] == 'complete' and not value['executed']: + _error('STATUS', 'Complete means the declared operation actually executed.') + if kind == 'derived' and value['status'] == 'verified' and not value['evidence']: + _error('EVIDENCE', 'Verified derived state requires exact artifact evidence.') + return value + + +def contract_hash(value): + validate_contract(value) + return object_hash(value) + + +def validate_project_bundle(project, scenes, nodes, assets, animations): + """Verify editor binding closure and digests, not native scene correctness.""" + for value in [project, *scenes, *nodes, *assets, *animations]: + validate_contract(value) + for group, kind in [(scenes, 'scene'), (nodes, 'node'), (assets, 'asset'), (animations, 'animation')]: + if any(x['kind'] != 'editor3d-' + kind for x in group): + _error('KIND', 'Bundle member has the wrong contract kind.', kind) + _unique(group, kind) + if project['kind'] != 'editor3d-project': + _error('KIND', 'Expected project contract.') + index = {x['id']: x for x in [*scenes, *nodes, *assets, *animations]} + if len(index) != len(scenes) + len(nodes) + len(assets) + len(animations): + _error('DUPLICATE', 'IDs are global within an editor project.') + for pointers, group in [(project['scenes'], scenes), (project['shared_assets'], assets), + (project['animation_clips'], animations)]: + if {x['id'] for x in pointers} != {x['id'] for x in group}: + _error('REFERENCE', 'Project pointers must exactly cover supplied bindings.') + for pointer in pointers: + if pointer['sha256'] != contract_hash(index[pointer['id']]): + _error('HASH', 'Binding digest differs from the project snapshot.', pointer['id']) + if project['id'] in index: + _error('DUPLICATE', 'Project identity cannot alias a child binding.') + scene_ids = {s['id'] for s in scenes} + asset_ids = {a['id'] for a in assets} + animation_ids = {a['id'] for a in animations} + node_ids = {n['id'] for n in nodes} + for scene in scenes: + if scene['project_id'] != project['id']: + _error('REFERENCE', 'Scene belongs to another project.', scene['id']) + local = {n['id']: n for n in nodes if n['scene_id'] == scene['id']} + if {p['id'] for p in scene['nodes']} != set(local): + _error('REFERENCE', 'Scene pointers must cover exactly their node bindings.', scene['id']) + for pointer in scene['nodes']: + if pointer['sha256'] != contract_hash(local[pointer['id']]): + _error('HASH', 'Node binding digest differs from scene snapshot.', pointer['id']) + if not set(scene['animations']) <= animation_ids: + _error('REFERENCE', 'Unknown scene animation.', scene['id']) + camera = scene['default_camera'] + if camera is not None and (camera not in local or local[camera]['node_type'] != 'camera'): + _error('REFERENCE', 'Default camera must name a camera in this scene.', scene['id']) + sibling_keys = set() + native_keys = set() + for node in local.values(): + native_key = (node['native']['owner'], node['native']['id'], + node['native']['file']['sha256'], node['native_node_id']) + if native_key in native_keys: + _error('DUPLICATE', 'Native node is already bound in this scene.', node['id']) + native_keys.add(native_key) + if node['parent'] is not None and node['parent'] not in local: + _error('REFERENCE', 'Parent is outside the node scene.', node['id']) + if not set(node['collections']) <= set(scene['collections']): + _error('REFERENCE', 'Node refers to an unknown collection.', node['id']) + if node['asset_id'] is not None and node['asset_id'] not in asset_ids: + _error('REFERENCE', 'Unknown asset binding.', node['id']) + if node['rig_id'] is not None and (node['rig_id'] not in asset_ids or index[node['rig_id']]['asset_type'] != 'rig'): + _error('RIG', 'Rig binding must identify a rig asset.', node['id']) + key = (node['parent'], node['sibling_order']) + if key in sibling_keys: + _error('ORDER', 'Sibling ordering must be unambiguous.', node['id']) + sibling_keys.add(key) + seen = set() + cursor = node['id'] + while cursor is not None: + if cursor in seen: + _error('CYCLE', 'Parent hierarchy contains a cycle.', cursor) + if cursor not in local: + _error('REFERENCE', 'Ancestor is outside the node scene.', cursor) + seen.add(cursor) + cursor = local[cursor]['parent'] + for node in nodes: + if node['scene_id'] not in scene_ids: + _error('REFERENCE', 'Node scene is absent.', node['id']) + for animation in animations: + if not set(animation['targets']) <= node_ids: + _error('REFERENCE', 'Animation target binding is absent.', animation['id']) + if animation['frame_rate'] != project['frame_rate']: + _error('RATE', 'Animation and project editor clocks differ.', animation['id']) + for scene in scenes: + if animation['id'] in scene['animations']: + if any(index[target]['scene_id'] != scene['id'] for target in animation['targets']): + _error('REFERENCE', 'Animation targets are outside the bound scene.', animation['id']) + if not scene['timeline']['start'] <= animation['interval']['start'] < animation['interval']['end'] <= scene['timeline']['end']: + _error('INTERVAL', 'Animation interval exceeds the bound scene timeline.', animation['id']) + return {'status': 'valid-bindings', 'project_sha256': contract_hash(project), + 'native_resources_verified': False, 'review_status': 'unreviewed'} + + +def validate_runtime_state(value, project, scenes, nodes): + """Validate nonpersistent editor state against its exact saved snapshot.""" + validate_contract(value) + validate_contract(project) + if project['kind'] != 'editor3d-project': + _error('KIND', 'Expected a saved editor project.') + for values, kind in [(scenes, 'scene'), (nodes, 'node')]: + for item in values: + validate_contract(item) + if item['kind'] != 'editor3d-' + kind: + _error('KIND', 'Runtime context contains the wrong contract kind.') + scene_map = {scene['id']: scene for scene in scenes} + if len(scene_map) != len(scenes) or {p['id'] for p in project['scenes']} != set(scene_map): + _error('REFERENCE', 'Runtime context does not match the saved scene set.') + for pointer in project['scenes']: + if pointer['sha256'] != contract_hash(scene_map[pointer['id']]): + _error('HASH', 'Runtime scene differs from the saved project binding.') + node_map = {node['id']: node for node in nodes} + if len(node_map) != len(nodes): + _error('DUPLICATE', 'Runtime node identities must be unique.') + if set(node_map) != {p['id'] for scene in scenes for p in scene['nodes']} or any( + node['scene_id'] not in scene_map for node in nodes): + _error('REFERENCE', 'Runtime context contains nodes outside the saved scene closure.') + for scene in scenes: + if scene['project_id'] != project['id']: + _error('REFERENCE', 'Runtime scene belongs to another project.') + if {p['id'] for p in scene['nodes']} != {n['id'] for n in nodes if n['scene_id'] == scene['id']}: + _error('REFERENCE', 'Runtime nodes do not match saved scene bindings.') + for pointer in scene['nodes']: + if pointer['sha256'] != contract_hash(node_map[pointer['id']]): + _error('HASH', 'Runtime node differs from the saved scene binding.') + if value['kind'] not in ('editor3d-session', 'editor3d-derived'): + _error('KIND', 'Expected session or derived editor state.') + if value['project_id'] != project['id']: + _error('REFERENCE', 'Editor state belongs to another project.') + expected_hash = value.get('base_project_sha256', value.get('project_sha256')) + if expected_hash != contract_hash(project): + _error('STALE', 'Editor state refers to a different saved snapshot.') + if value['kind'] == 'editor3d-derived': + if value['frame_rate'] != project['frame_rate']: + _error('RATE', 'Derived state and project editor clocks differ.') + if not any(scene['timeline']['start'] <= value['frame'] <= scene['timeline']['end'] + for scene in scenes): + _error('FRAME', 'Derived frame is outside the saved scene boundary ranges.') + if value['kind'] == 'editor3d-session': + if value['base_revision'] != project['revision']: + _error('STALE', 'Editor session revision is stale.') + scene_map = {scene['id']: scene for scene in scenes} + if value['active_scene'] is not None and value['active_scene'] not in scene_map: + _error('REFERENCE', 'Session active scene is absent.') + permitted = {node['id'] for node in nodes if node['scene_id'] == value['active_scene']} + if not set(value['selection']) <= permitted: + _error('REFERENCE', 'Selection must belong to the active scene.') + if value['active_scene'] is not None: + interval = scene_map[value['active_scene']]['timeline'] + if not interval['start'] <= value['playhead'] <= interval['end']: + _error('FRAME', 'Playhead is outside the scene including its terminal boundary.') + return value + + +def migrate_document(value): + """Identity migration only; unknown versions are explicitly unsupported.""" + return deepcopy(validate_contract(value)) + + +def _ts(schema): + if '$ref' in schema: + return schema['$ref'].split('/')[-1] + if 'const' in schema: + return json.dumps(schema['const'], separators=(',', ':')) + if 'enum' in schema: + return ' | '.join(json.dumps(x) for x in schema['enum']) + if 'anyOf' in schema: + return '(' + ' | '.join(_ts(x) for x in schema['anyOf']) + ')' + kind = schema.get('type') + if kind == 'object': + fields = schema.get('properties', {}) + required = schema.get('required', []) + return '{ ' + '; '.join(json.dumps(k) + ('' if k in required else '?') + ': ' + _ts(v) for k, v in fields.items()) + ' }' + if kind == 'array': + item = _ts(schema['items']) + if schema.get('minItems') == schema.get('maxItems'): + return '[' + ', '.join(item for _ in range(schema['minItems'])) + ']' + return 'Array<' + item + '>' + return {'string': 'string', 'integer': 'number', 'number': 'number', + 'boolean': 'boolean', 'null': 'null'}[kind] + + +def typescript(): + return '// Generated by scripts/editor3d_contracts.py; runtime admission is still required.\n' + '\n'.join( + 'export type ' + name + ' = ' + _ts(schema) + ';' for name, schema in _definitions().items()) + '\n' + + +def generated_files(): + root = Path(__file__).resolve().parents[1] + return {root / 'profiles/editor3d.schema.json': json.dumps(schemas()['editor3d.schema.json'], indent=2) + '\n', + root / 'assets/editor3d/contracts.ts': typescript()} + + +def load_document(path): + """Read a bounded regular descriptor; reject nonregular and symlink inputs.""" + flags = (os.O_RDONLY | getattr(os, 'O_NONBLOCK', 0) + | getattr(os, 'O_NOFOLLOW', 0) | getattr(os, 'O_BINARY', 0)) + try: + if Path(path).is_symlink(): + _error('FILE', 'Editor document input must not be a symbolic link.') + fd = os.open(path, flags) + with os.fdopen(fd, 'rb') as stream: + observed = os.fstat(stream.fileno()) + if not stat.S_ISREG(observed.st_mode): + _error('FILE', 'Editor documents must be regular files.') + if not 0 < observed.st_size <= MAX_DOCUMENT_BYTES: + _error('LIMIT', 'Editor document must contain 1 byte to 4 MiB.') + payload = stream.read(MAX_DOCUMENT_BYTES + 1) + if len(payload) > MAX_DOCUMENT_BYTES or len(payload) != observed.st_size: + _error('LIMIT', 'Editor document size changed during bounded admission.') + except OSError as exc: + raise RigError('EDITOR3D_FILE', 'Cannot open editor document: ' + str(exc), + stage='editor3d', asset=str(path)) from exc + return decode_json(payload) + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--check', action='store_true') + parser.add_argument('--validate', type=Path) + args = parser.parse_args(argv) + if args.validate: + value = load_document(args.validate) + print(json.dumps(dict(status='valid-contract', sha256=contract_hash(value), native_resources_verified=False))) + return + for path, content in generated_files().items(): + if args.check: + require(path.is_file() and path.read_text(encoding='utf-8') == content, + 'EDITOR3D_DRIFT', 'Generated editor contract differs.', asset=str(path)) + else: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding='utf-8') + + +if __name__ == '__main__': + try: + main() + except RigError as exc: + print(json.dumps(exc.report)) + raise SystemExit(1) diff --git a/scripts/generate_schemas.py b/scripts/generate_schemas.py index 9b4d3ae..933020d 100644 --- a/scripts/generate_schemas.py +++ b/scripts/generate_schemas.py @@ -55,11 +55,13 @@ def build(): from decomposition_spec import schemas as decomposition_schemas from rig_contracts import schemas as rig_schemas from model3d_contracts import schemas as model3d_schemas + from editor3d_contracts import schemas as editor3d_schemas return {ROOT/'profiles/profile.schema.json':profile,ROOT/'profiles/asset-inputs.schema.json':inputs, ROOT/'profiles/style-catalog.schema.json':catalog_schema(), **{ROOT / 'profiles' / name: value for name, value in decomposition_schemas().items()}, **{ROOT / 'profiles' / name: value for name, value in rig_schemas().items()}, - **{ROOT / 'profiles' / name: value for name, value in model3d_schemas().items()}} + **{ROOT / 'profiles' / name: value for name, value in model3d_schemas().items()}, + **{ROOT / 'profiles' / name: value for name, value in editor3d_schemas().items()}} if __name__=='__main__': diff --git a/scripts/test_rig_gate.py b/scripts/test_rig_gate.py index d179eea..5a85ec3 100644 --- a/scripts/test_rig_gate.py +++ b/scripts/test_rig_gate.py @@ -7,7 +7,7 @@ ROOT=Path(__file__).resolve().parents[1] GROUPS={ - 'schema-math':['test_rig_math.py','test_rig_contracts.py','test_rig_compatibility.py'], + 'schema-math':['test_rig_math.py','test_rig_contracts.py','test_rig_compatibility.py','test_rig_editor3d_contracts.py'], 'skinning':['test_rig_skinning.py'], 'animation-ik':['test_rig_controls.py','test_rig_animation.py','test_rig_specializations.py','test_rig_morph_authoring.py'], 'interoperability':['test_rig_interop.py','test_rig_import_transforms.py'], diff --git a/tests/test_rig_editor3d_contracts.py b/tests/test_rig_editor3d_contracts.py new file mode 100644 index 0000000..824f700 --- /dev/null +++ b/tests/test_rig_editor3d_contracts.py @@ -0,0 +1,527 @@ +"""Independent Editor3D contract, admission, and coordinate regression checks. + +The retained examples are engineering bindings. These tests never promote a +valid binding or a parsed native manifest into scene execution or art approval. +""" +from copy import deepcopy +from fractions import Fraction +import hashlib +import importlib.util +import json +import math +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest +from unittest.mock import patch + +from jsonschema import Draft202012Validator, FormatChecker + +from editor3d_contracts import (MAX_DOCUMENT_BYTES, READ_OPERATIONS, + WRITE_OPERATIONS, contract_hash, generated_files, load_document, migrate_document, schemas, + validate_contract, validate_project_bundle, validate_runtime_state) +from editor3d_adapters import (euler_to_quaternion, frame_time, + quaternion_to_euler, transform_to_blender, validate_api_request, + verify_native_reference) +from rig_errors import RigError + +ROOT = Path(__file__).resolve().parents[1] +EXAMPLES = ROOT / 'examples/editor3d-contracts' +TOKEN = 'editor-regression-token-0123456789' +ORIGIN = 'http://127.0.0.1:8765' + + +def canonical_hash(value): + return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(',', ':'), + allow_nan=False).encode()).hexdigest() + + +def example(name): + return json.loads((EXAMPLES / (name + '.json')).read_text()) + + +def rehash_bundle(project, scenes, nodes, assets, animations): + """Rebind edited fixtures so semantic probes do not stop at stale hashes.""" + for scene in scenes: + scene['nodes'] = [dict(id=n['id'], sha256=canonical_hash(n)) + for n in nodes if n['scene_id'] == scene['id']] + for field, resources in [('scenes', scenes), ('shared_assets', assets), + ('animation_clips', animations)]: + project[field] = [dict(id=x['id'], sha256=canonical_hash(x)) for x in resources] + return project, scenes, nodes, assets, animations + + +class EditorContractTests(unittest.TestCase): + def test_all_eleven_documents_roundtrip_without_mutating(self): + values = [example(k) for k in ('project', 'scene', 'node', 'asset', 'animation', + 'command', 'revision', 'session', 'derived', 'request', 'response')] + schema = schemas()['editor3d.schema.json'] + Draft202012Validator.check_schema(schema) + validator = Draft202012Validator(schema, format_checker=FormatChecker()) + for value in values: + with self.subTest(kind=value['kind']): + original = deepcopy(value) + validator.validate(value) + validate_contract(value) + self.assertEqual(value, original) + self.assertEqual(contract_hash(value), canonical_hash(value)) + migrated = migrate_document(value) + self.assertEqual(migrated, original) + self.assertIsNot(migrated, value) + self.assertEqual(contract_hash(json.loads(json.dumps(value))), canonical_hash(value)) + + def test_generated_json_schema_and_typescript_match_authority(self): + for path, content in generated_files().items(): + self.assertEqual(path.read_text(), content, str(path)) + typescript = (ROOT / 'assets/editor3d/contracts.ts').read_text() + for kind in ('project', 'scene', 'node', 'asset', 'animation', 'command', + 'revision', 'session', 'derived', 'request', 'response'): + self.assertIn('export type ' + kind + ' = ', typescript) + self.assertIn('runtime admission is still required', typescript) + + def test_every_document_is_closed_versioned_and_required(self): + for path in EXAMPLES.glob('*.json'): + original = json.loads(path.read_text()) + if not original.get('kind', '').startswith('editor3d-'): + continue + for mutation in ('extra', 'version', 'required'): + with self.subTest(kind=original['kind'], mutation=mutation): + value = deepcopy(original) + if mutation == 'extra': + value['unexpected'] = 'rejected' + elif mutation == 'version': + value['schema_version'] = 2 + else: + del value['kind'] + with self.assertRaises(RigError): + validate_contract(value) + + def test_schema_exports_cannot_mutate_later_validation(self): + exported = schemas() + exported['editor3d.schema.json']['$defs']['project']['additionalProperties'] = True + value = example('project'); value['hidden_session'] = {} + with self.assertRaises(RigError): + validate_contract(value) + + def test_nested_unknown_and_nonfinite_values_are_rejected(self): + values = [] + value = example('project'); value['provenance']['fixture'] = 'true'; values.append(value) + value = example('node'); value['native']['execute'] = 'untrusted.py'; values.append(value) + for bad in (math.nan, math.inf, -math.inf): + value = example('session'); value['viewport']['translation'][0] = bad; values.append(value) + for value in values: + with self.subTest(value=value): + with self.assertRaises(RigError): + validate_contract(value) + + def test_persistent_session_and_derived_state_are_separate(self): + for kind, field, value in [('project', 'selection', ['node']), + ('scene', 'playhead', 3), ('node', 'world_matrix', [1] * 16), + ('session', 'nodes', []), ('derived', 'unsaved_command_ids', [])]: + doc = example(kind); doc[field] = value + with self.subTest(kind=kind, field=field), self.assertRaises(RigError): + validate_contract(doc) + + def test_local_semantics_reject_wrong_clock_provenance_and_quaternion(self): + mutations = [] + value = example('project'); value['updated_at'] = '2026-10-24T23:59:59Z'; mutations.append(value) + value = example('project'); value['provenance']['fixture'] = False; mutations.append(value) + value = example('asset'); value['provenance']['fixture'] = False; mutations.append(value) + value = example('session'); value['viewport']['rotation'] = [0, 0, 0, 2]; mutations.append(value) + value = example('animation'); value['interval']['end'] = 0; mutations.append(value) + value = example('animation'); value['frame_rate'] = {'numerator': 48, 'denominator': 2}; mutations.append(value) + value = example('node'); value['node_type'] = 'bone'; mutations.append(value) + for value in mutations: + with self.subTest(kind=value['kind']), self.assertRaises(RigError): + validate_contract(value) + + def test_native_file_paths_reject_nonportable_and_remote_inputs(self): + for path in ('../outside.json', '/tmp/outside.json', 'C:/outside.json', + 'https://example.com/a.json', 'a\\b.json', 'a/../b.json'): + value = example('asset'); value['native']['file']['path'] = path + with self.subTest(path=path), self.assertRaises(RigError): + validate_contract(value) + + def test_rig_asset_binding_requires_actual_rig_owner(self): + value = example('asset'); value['asset_type'] = 'rig' + with self.assertRaises(RigError): validate_contract(value) + + def test_hash_identifiers_and_exact_versions_reject_ambiguous_spellings(self): + for field, bad in [('id', 'asset\n'), ('version', '01.0.0'), + ('version', '1.0.0\n')]: + value = example('asset'); value['native'][field] = bad + with self.subTest(field=field, bad=bad), self.assertRaises(RigError): + validate_contract(value) + value = example('asset'); value['native']['file']['sha256'] += '\n' + with self.assertRaises(RigError): + validate_contract(value) + + def test_boolean_numeric_values_rejected_but_integral_json_numbers_work(self): + value = example('project'); value['frame_rate'] = {'numerator': 24.0, 'denominator': 1.0} + validate_contract(value) + self.assertEqual(frame_time(720, value['frame_rate']), Fraction(30)) + for kind, field in [('project', 'revision'), ('node', 'sibling_order'), + ('session', 'playhead')]: + value = example(kind); value[field] = True + with self.subTest(kind=kind), self.assertRaises(RigError): + validate_contract(value) + + def test_status_does_not_imply_execution_or_visual_approval(self): + value = example('response') + for status, executed in [('accepted', True), ('complete', False), + ('rejected', True), ('unavailable', True)]: + bad = deepcopy(value); bad.update(status=status, executed=executed) + with self.subTest(status=status), self.assertRaises(RigError): + validate_contract(bad) + value['review_status'] = 'approved' + with self.assertRaises(RigError): + validate_contract(value) + for kind in ('project', 'response'): + value = example(kind); self.assertEqual(value['review_status'], 'unreviewed') + value = example('derived'); value['status'] = 'verified' + with self.assertRaises(RigError): + validate_contract(value) + + def test_all_service_operations_have_closed_admission(self): + original = example('request') + global_ops = {'projects.list', 'projects.create', 'render.capabilities', 'recovery.list'} + project_ops = {'projects.open', 'projects.get', 'projects.validate', 'projects.save', + 'projects.close', 'revisions.create', 'revisions.list', 'scenes.list', + 'scenes.create', 'assets.list', 'assets.import', 'render.create', + 'recovery.inspect', 'recovery.dependencies'} + argument_ops = {'projects.create', 'projects.save', 'scenes.create', 'scenes.command', + 'assets.import', 'assets.relink', 'render.create', 'render.retry', 'recovery.recover'} + for operation in READ_OPERATIONS + WRITE_OPERATIONS: + with self.subTest(operation=operation): + request = deepcopy(original); request['operation'] = operation + request['project_id'] = None if operation in global_ops else 'project' + request['target_id'] = None if operation in global_ops | project_ops else 'resource' + request['argument'] = original['argument'] if operation in argument_ops else None + if operation in READ_OPERATIONS or operation == 'projects.create': + request.update(expected_revision=None, expected_project_sha256=None) + if operation in READ_OPERATIONS: + request['idempotency_key'] = None + validate_contract(request) + original['operation'] = 'shell.execute' + with self.assertRaises(RigError): + validate_contract(original) + + def test_mutations_require_explicit_idempotency_and_revision(self): + for field in ('project_id', 'expected_revision', 'expected_project_sha256', 'idempotency_key'): + value = example('request'); value[field] = None + with self.subTest(field=field), self.assertRaises(RigError): + validate_contract(value) + value = example('request'); value['operation'] = 'projects.create' + with self.assertRaises(RigError): + validate_contract(value) + + def bundle(self): + return example('project'), [example('scene')], [example('node')], [example('asset')], [] + + def test_binding_closure_reports_only_binding_acceptance(self): + bundle = self.bundle(); original = deepcopy(bundle) + result = validate_project_bundle(*bundle) + self.assertEqual(result['status'], 'valid-bindings') + self.assertFalse(result['native_resources_verified']) + self.assertEqual(result['review_status'], 'unreviewed') + self.assertEqual(bundle, original) + + def test_bundle_rejects_missing_extra_duplicate_and_stale_bindings(self): + bundle = self.bundle(); bundle[1].clear() + with self.assertRaises(RigError): validate_project_bundle(*bundle) + bundle = self.bundle(); bundle[2].append(deepcopy(bundle[2][0])) + with self.assertRaises(RigError): validate_project_bundle(*bundle) + bundle = self.bundle(); bundle[3][0]['original_filename'] = 'changed.json' + with self.assertRaises(RigError): validate_project_bundle(*bundle) + bundle = self.bundle(); bundle[0]['scenes'].append(deepcopy(bundle[0]['scenes'][0])) + with self.assertRaises(RigError): validate_project_bundle(*bundle) + + def test_bundle_rejects_dangling_parent_camera_asset_rig_and_collection(self): + cases = [('parent', 'unknown'), ('asset_id', 'unknown'), ('rig_id', 'asset'), + ('collections', ['unknown'])] + for field, value in cases: + bundle = self.bundle(); bundle[2][0][field] = value + with self.subTest(field=field), self.assertRaises(RigError): + validate_project_bundle(*rehash_bundle(*bundle)) + bundle = self.bundle(); bundle[1][0]['default_camera'] = 'node' + with self.assertRaises(RigError): validate_project_bundle(*rehash_bundle(*bundle)) + bundle = self.bundle(); bundle[1][0]['animations'] = ['missing'] + with self.assertRaises(RigError): validate_project_bundle(*rehash_bundle(*bundle)) + + def test_bundle_rejects_parent_cycle_and_ambiguous_sibling_order(self): + bundle = self.bundle(); bundle[2][0]['parent'] = 'node' + with self.assertRaises(RigError): validate_project_bundle(*rehash_bundle(*bundle)) + bundle = self.bundle(); duplicate = deepcopy(bundle[2][0]); duplicate['id'] = 'second' + duplicate['native_node_id'] = 'native-second'; bundle[2].append(duplicate) + with self.assertRaises(RigError): validate_project_bundle(*rehash_bundle(*bundle)) + + def test_bundle_rejects_global_collisions_and_cross_scene_parents(self): + bundle = self.bundle(); bundle[3][0]['id'] = 'node' + with self.assertRaises(RigError): validate_project_bundle(*rehash_bundle(*bundle)) + bundle = self.bundle(); second = deepcopy(bundle[1][0]); second['id'] = 'second-scene' + bundle[1].append(second); node = deepcopy(bundle[2][0]); node['id'] = 'second-node' + node['scene_id'] = 'second-scene'; node['parent'] = 'node'; bundle[2].append(node) + with self.assertRaises(RigError): validate_project_bundle(*rehash_bundle(*bundle)) + + def test_animation_targets_must_exist(self): + bundle = self.bundle(); animation = example('animation'); animation['targets'] = ['missing'] + bundle[4].append(animation); bundle[1][0]['animations'] = ['animation'] + with self.assertRaises(RigError): validate_project_bundle(*rehash_bundle(*bundle)) + + + def test_animation_clock_and_scene_interval_must_agree(self): + for mutation in ('rate', 'interval', 'outside-scene'): + bundle = self.bundle(); animation = example('animation') + if mutation == 'rate': animation['frame_rate']['numerator'] = 30 + elif mutation == 'interval': animation['interval']['end'] = 721 + else: + second = deepcopy(bundle[1][0]); second['id'] = 'second-scene' + bundle[1].append(second) + bundle[2][0]['scene_id'] = 'second-scene' + bundle[4].append(animation); bundle[1][0]['animations'] = ['animation'] + with self.subTest(mutation=mutation), self.assertRaises(RigError): + validate_project_bundle(*rehash_bundle(*bundle)) + + def test_duplicate_native_node_binding_cannot_alias_another_editor_node(self): + bundle = self.bundle(); second = deepcopy(bundle[2][0]) + second.update(id='second-node', sibling_order=1) + bundle[2].append(second) + with self.assertRaises(RigError): validate_project_bundle(*rehash_bundle(*bundle)) + + def test_orphaned_ancestor_reports_typed_error_before_any_write(self): + bundle = self.bundle(); first = bundle[2][0]; first['parent'] = 'second-node' + second = deepcopy(first); second.update(id='second-node', parent='missing', + native_node_id='second-native', sibling_order=1) + bundle[2].append(second) + original = deepcopy(bundle) + with self.assertRaises(RigError): validate_project_bundle(*rehash_bundle(*bundle)) + self.assertEqual(bundle[2], original[2]) + + def test_session_selection_playhead_and_saved_snapshot_are_bound(self): + project, scenes, nodes, _, _ = self.bundle() + session = example('session') + validate_runtime_state(session, project, scenes, nodes) + for field, value in [('base_revision', 1), ('base_project_sha256', 'f' * 64), + ('project_id', 'other-project'), ('active_scene', 'missing'), + ('selection', ['missing']), ('playhead', 721)]: + bad = deepcopy(session); bad[field] = value + with self.subTest(field=field), self.assertRaises(RigError): + validate_runtime_state(bad, project, scenes, nodes) + session['playhead'] = 720 + validate_runtime_state(session, project, scenes, nodes) + session['active_scene'] = None + with self.assertRaises(RigError): validate_runtime_state(session, project, scenes, nodes) + + def test_runtime_state_rejects_nodes_outside_saved_scene_context(self): + project, scenes, nodes, _, _ = self.bundle() + extra = deepcopy(nodes[0]); extra.update(id='orphan', scene_id='absent-scene') + nodes.append(extra) + for kind in ('session', 'derived'): + with self.subTest(kind=kind), self.assertRaises(RigError): + validate_runtime_state(example(kind), project, scenes, nodes) + + def test_derived_clock_and_frame_are_bound_to_saved_project(self): + project, scenes, nodes, _, _ = self.bundle() + state = example('derived'); state['frame'] = 720 + validate_runtime_state(state, project, scenes, nodes) + for field, value in [('frame', 721), ('frame_rate', {'numerator': 30, 'denominator': 1})]: + bad = deepcopy(state); bad[field] = value + with self.subTest(field=field), self.assertRaises(RigError): + validate_runtime_state(bad, project, scenes, nodes) + + def test_derived_and_session_context_reject_stale_or_absent_nodes(self): + for kind in ('derived', 'session'): + project, scenes, nodes, _, _ = self.bundle() + state = example(kind) + validate_runtime_state(state, project, scenes, nodes) + with self.subTest(kind=kind, mutation='missing-node'), self.assertRaises(RigError): + validate_runtime_state(state, project, scenes, []) + nodes[0]['locked'] = True + with self.subTest(kind=kind, mutation='stale-node'), self.assertRaises(RigError): + validate_runtime_state(state, project, scenes, nodes) + + +class EditorAdapterTests(unittest.TestCase): + def api(self, value=None, **changes): + value = example('request') if value is None else value + payload = json.dumps(value).encode() + arguments = dict(authorization='Bearer ' + TOKEN, token=TOKEN, origin=ORIGIN, + server_origin=ORIGIN, content_length=len(payload), current_revision=0, + current_project_sha256=example('request')['expected_project_sha256']) + arguments.update(changes) + return validate_api_request(payload, **arguments) + + def test_authenticated_admission_is_read_only_and_revision_bound(self): + value = example('request'); original = deepcopy(value) + result = self.api(value) + self.assertEqual(result['status'], 'admitted') + self.assertFalse(result['executed']) + self.assertEqual(value, original) + for changes in ({'current_revision': 1}, {'current_project_sha256': 'f' * 64}, + {'current_revision': None}): + with self.subTest(changes=changes), self.assertRaises(RigError): self.api(**changes) + + def test_authentication_origin_content_type_and_length_rejections(self): + for changes in ({'authorization': None}, {'authorization': 'Bearer wrong'}, + {'token': 'short'}, {'origin': 'http://evil.invalid'}, + {'server_origin': 'http://0.0.0.0:8765'}, {'server_origin': 'http://localhost'}, + {'server_origin': 'http://user@localhost:8765'}, + {'server_origin': 'https://localhost:8765'}, {'content_type': 'text/plain'}, + {'content_length': 1}, {'content_length': True}, {'content_length': MAX_DOCUMENT_BYTES + 1}): + with self.subTest(changes=changes), self.assertRaises(RigError): self.api(**changes) + self.assertFalse(self.api(origin=None)['executed']) + + def test_duplicate_keys_invalid_utf8_and_nonfinite_json_rejected(self): + payloads = [b'{"kind":"editor3d-request","kind":"editor3d-project"}', + b'{"x":"\xff"}', b'{"x":NaN}', b'{"x":Infinity}'] + for payload in payloads: + with self.subTest(payload=payload), self.assertRaises(RigError): + validate_api_request(payload, authorization='Bearer ' + TOKEN, token=TOKEN, + origin=ORIGIN, server_origin=ORIGIN, content_length=len(payload)) + + def test_exact_half_open_clock_and_ntsc_boundaries(self): + self.assertEqual(frame_time(0, {'numerator': 24, 'denominator': 1}), 0) + self.assertEqual(frame_time(719, {'numerator': 24, 'denominator': 1}), Fraction(719, 24)) + self.assertEqual(frame_time(720, {'numerator': 24, 'denominator': 1}), 30) + self.assertEqual(frame_time(720.0, {'numerator': 24.0, 'denominator': 1.0}), 30) + self.assertEqual(frame_time(30000, {'numerator': 30000, 'denominator': 1001}), 1001) + for frame, fps in [(-1, {'numerator': 24, 'denominator': 1}), + (True, {'numerator': 24, 'denominator': 1}), + (1, {'numerator': 48, 'denominator': 2}), + (1, {'numerator': 24, 'denominator': 0}), + (1, {'numerator': True, 'denominator': 1})]: + with self.subTest(frame=frame, fps=fps), self.assertRaises(RigError): frame_time(frame, fps) + + def test_actual_native_manifest_hash_identity_and_version(self): + reference = example('asset')['native'] + report = verify_native_reference(reference, ROOT) + self.assertEqual(report['status'], 'valid-native-contract') + self.assertFalse(report['transitive_resources_verified']) + self.assertEqual(report['review_status'], 'unreviewed') + for field, value in [('id', 'other'), ('version', '9.9.9'), ('native_kind', 'model3d-job')]: + altered = deepcopy(reference); altered[field] = value + with self.subTest(field=field), self.assertRaises(RigError): verify_native_reference(altered, ROOT) + for field, value in [('sha256', '0' * 64), ('bytes', reference['file']['bytes'] + 1), + ('media_type', 'application/octet-stream')]: + altered = deepcopy(reference); altered['file'][field] = value + with self.subTest(field=field), self.assertRaises(RigError): verify_native_reference(altered, ROOT) + + def test_unintegrated_owner_is_explicitly_unavailable(self): + reference = example('scene')['native'] + with self.assertRaises(RigError) as raised: + verify_native_reference(reference, ROOT) + self.assertEqual(raised.exception.report['code'], 'EDITOR3D_UNAVAILABLE') + + def test_native_paths_reject_symlinks_without_reading_target(self): + with tempfile.TemporaryDirectory() as td: + root = Path(td); path = root / 'manifest.json' + try: + path.symlink_to(ROOT / 'examples/model3d-contracts/robotic.json') + except (OSError, NotImplementedError) as exc: + self.skipTest('Symlink creation unavailable: ' + str(exc)) + reference = example('asset')['native']; reference['file']['path'] = path.name + with self.assertRaises(RigError): verify_native_reference(reference, root) + + def test_coordinate_basis_maps_y_up_and_preserves_input(self): + value = {'translation': [1, 2, 3], 'rotation': [0, 0, 0, 1], 'scale': [1, 1, 1]} + original = deepcopy(value); matrix = transform_to_blender(value) + self.assertEqual([row[3] for row in matrix[:3]], [1, -3, 2]) + self.assertEqual(value, original) + self.assertEqual(matrix[3], [0, 0, 0, 1]) + value['rotation'] = [0, 0, 0, 2] + with self.assertRaises(RigError): transform_to_blender(value) + + def test_euler_quaternion_roundtrip_including_gimbal_lock(self): + for angles in ([0, 0, 0], [.2, -.4, 1.0], [.4, math.pi / 2, -.6], + [.4, -math.pi / 2, -.6]): + with self.subTest(angles=angles): + q = euler_to_quaternion(angles) + result = quaternion_to_euler(q) + recovered = euler_to_quaternion(result['euler']) + self.assertAlmostEqual(abs(sum(a * b for a, b in zip(q, recovered))), 1, places=10) + self.assertEqual(result['convention'], 'extrinsic-XYZ-radians') + if abs(angles[1]) == math.pi / 2: + self.assertTrue(result['gimbal_lock']) + q = euler_to_quaternion([math.pi / 2, 0, 0]) + self.assertAlmostEqual(q[0], math.sqrt(.5)) + self.assertAlmostEqual(q[3], math.sqrt(.5)) + + def test_document_reader_rejects_real_file_growth_after_metadata_check(self): + with tempfile.TemporaryDirectory() as td: + path = Path(td) / 'input.json' + path.write_text('{}') + real_fstat = os.fstat + def grow_after_stat(fd): + initial = real_fstat(fd) + with path.open('ab') as output: + output.write(b' ' * (MAX_DOCUMENT_BYTES + 1)) + return initial + with patch('editor3d_contracts.os.fstat', side_effect=grow_after_stat): + with self.assertRaises(RigError) as raised: + load_document(path) + self.assertIn(raised.exception.report['code'], ('EDITOR3D_LIMIT', 'EDITOR3D_FILE')) + + def test_cli_accepts_normal_document_and_rejects_fifo_without_blocking(self): + result = subprocess.run([sys.executable, 'scripts/editor3d_contracts.py', '--validate', + str(EXAMPLES / 'project.json')], cwd=ROOT, capture_output=True, text=True, timeout=5) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertFalse(json.loads(result.stdout)['native_resources_verified']) + if not hasattr(os, 'mkfifo'): + self.skipTest('Named pipes are unavailable on this platform.') + with tempfile.TemporaryDirectory() as td: + fifo = Path(td) / 'input.json'; os.mkfifo(fifo) + result = subprocess.run([sys.executable, 'scripts/editor3d_contracts.py', '--validate', + str(fifo)], cwd=ROOT, capture_output=True, text=True, timeout=5) + self.assertNotEqual(result.returncode, 0) + diagnostic = json.loads(result.stdout) + self.assertTrue(diagnostic['code'].startswith('EDITOR3D_')) + self.assertNotIn('Traceback', result.stderr) + + def test_timestamp_validation_requires_no_optional_formatter(self): + code = """import importlib.abc,json,sys +sys.path.insert(0, 'scripts') +class Block(importlib.abc.MetaPathFinder): + def find_spec(self,fullname,path=None,target=None): + if fullname.split('.')[0] == 'rfc3339_validator': + raise ImportError('Optional formatter intentionally absent') +sys.meta_path.insert(0,Block()) +from editor3d_contracts import validate_contract +from rig_errors import RigError +for name,fields in [('project',('created_at','updated_at')),('revision',('created_at',))]: + path='examples/editor3d-contracts/'+name+'.json' + original=json.load(open(path)) + validate_contract(original) + for field in fields: + for timestamp in ('invalid','2026-10-25','2026-10-25T00:00:00','2026-13-25T00:00:00Z', + '2026-10-25T25:00:00Z','2026-10-25T00:00:00+24:00', + '2026-10-25T00:00:00+01:99','2026-10-25T00:00:00+00:60'): + value=json.load(open(path));value[field]=timestamp + try: validate_contract(value) + except RigError as error: + assert error.report['code'].startswith('EDITOR3D_'),error.report + else: raise AssertionError((name,field,timestamp)) +""" + result = subprocess.run([sys.executable, '-c', code], cwd=ROOT, + text=True, capture_output=True, timeout=5) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_schema_and_admission_import_without_optional_graphics_dependencies(self): + code = """import importlib.abc,sys +sys.path.insert(0, 'scripts') +class Block(importlib.abc.MetaPathFinder): + def find_spec(self,fullname,path=None,target=None): + if fullname.split('.')[0] in ('numpy','bpy','pygltflib','playwright'): + raise ImportError('Forbidden optional import: '+fullname) +sys.meta_path.insert(0,Block()) +import editor3d_contracts,editor3d_adapters +assert len(editor3d_contracts.schemas()['editor3d.schema.json']['oneOf']) == 11 +assert editor3d_adapters.frame_time(720,{'numerator':24,'denominator':1}) == 30 +""" + result = subprocess.run([sys.executable, '-c', code], cwd=ROOT, text=True, capture_output=True) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + +if __name__ == '__main__': + unittest.main()