cozyclay 1.9.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (286) hide show
  1. package/CHANGELOG.md +171 -0
  2. package/bin/agent/agent-routes.mjs +74 -70
  3. package/bin/agent/agent-runner.mjs +10 -3
  4. package/bin/agent/motion-runtime.mjs +32 -260
  5. package/bin/agent/providers.mjs +46 -3
  6. package/bin/agent/session-store.mjs +41 -5
  7. package/bin/agent/studio-prompt.mjs +1 -1
  8. package/bin/agent/studio-tools.mjs +48 -11
  9. package/bin/cozyclay.mjs +26 -8
  10. package/bin/live/cli.mjs +22 -3
  11. package/bin/mcp-runtime.mjs +35 -20
  12. package/bin/telemetry-state.mjs +5 -2
  13. package/bin/update-check.mjs +15 -0
  14. package/dist/app/index.html +6 -6
  15. package/dist/assets/analytics-MVZOEW8O.js +1 -0
  16. package/dist/assets/app-C1sdVbg4.css +1 -0
  17. package/dist/assets/app-CA55tcI3.js +4876 -0
  18. package/dist/assets/{demo-B-eg1CT8.js → demo-ByeYp-dY.js} +1 -1
  19. package/dist/assets/{first-shot-handoff-CGgnvNu8.js → first-shot-handoff-DgT28ZiV.js} +1 -1
  20. package/dist/assets/{landing-IbRp3FnQ.js → landing-BaXkpFip.js} +1 -1
  21. package/dist/assets/shot-prompt-CbqWcWYi.css +1 -0
  22. package/dist/assets/shot-prompt-Yrt99wZx.js +17 -0
  23. package/dist/assets/{ticket-BgIzOBOf.js → ticket-BcmsnHn5.js} +1 -1
  24. package/dist/assets/workflow-DRdwoDBT.css +1 -0
  25. package/dist/assets/workflow-dNPGY9xz.js +179 -0
  26. package/dist/cozyclay-package.json +1 -1
  27. package/dist/fonts/IBM-Plex-Sans-OFL.txt +92 -0
  28. package/dist/fonts/JetBrains-Mono-OFL.txt +93 -0
  29. package/dist/fonts/README.md +13 -9
  30. package/dist/fonts/ibm-plex-sans-400-latin.woff2 +0 -0
  31. package/dist/fonts/ibm-plex-sans-500-latin.woff2 +0 -0
  32. package/dist/fonts/ibm-plex-sans-600-latin.woff2 +0 -0
  33. package/dist/fonts/jetbrains-mono-400-latin.woff2 +0 -0
  34. package/dist/fonts/jetbrains-mono-500-latin.woff2 +0 -0
  35. package/dist/index.html +7 -7
  36. package/dist/privacy/index.html +13 -8
  37. package/dist/sitemap.xml +6 -6
  38. package/dist/workflow/index.html +5 -5
  39. package/mcp/LIVE-PROTOCOL.md +1 -1
  40. package/mcp/live-hub.mjs +35 -93
  41. package/mcp/mesh-file.mjs +48 -0
  42. package/mcp/server.mjs +6 -77
  43. package/mcp/tool-handlers.mjs +224 -377
  44. package/package.json +2 -1
  45. package/src/App.jsx +1273 -9075
  46. package/src/analytics.js +393 -9
  47. package/src/app-context.js +211 -0
  48. package/src/app-stage.jsx +25 -24
  49. package/src/ardy/auto-fix-panel.css +108 -0
  50. package/src/ardy/collision-blockers.js +9 -3
  51. package/src/ardy/fix-collisions.js +10 -66
  52. package/src/ardy/ground.js +4 -4
  53. package/src/ardy/ik-drag.js +101 -0
  54. package/src/ardy/ik-key-json.js +43 -0
  55. package/src/ardy/ik.js +122 -76
  56. package/src/ardy/physics-panel.css +2 -0
  57. package/src/ardy/physics-panel.jsx +17 -5
  58. package/src/ardy/platform-fit-panel.css +64 -0
  59. package/src/ardy/platform-fit-panel.jsx +40 -0
  60. package/src/ardy/platform-fit.js +326 -0
  61. package/src/ardy/playback.js +166 -20
  62. package/src/ardy/range-pin.js +299 -0
  63. package/src/ardy/timeline-coordinates.js +26 -0
  64. package/src/ardy/timeline.css +1199 -0
  65. package/src/ardy/timeline.jsx +149 -41
  66. package/src/ardy/waypoints.js +2 -2
  67. package/src/asset-pane.css +462 -0
  68. package/src/asset-pane.jsx +461 -232
  69. package/src/command-bus.js +349 -0
  70. package/src/commands/ai.js +31 -0
  71. package/src/commands/cast.js +140 -0
  72. package/src/commands/elements/character.js +13 -0
  73. package/src/commands/elements/motion.js +14 -0
  74. package/src/commands/elements/object.js +22 -0
  75. package/src/commands/elements/scene.js +16 -0
  76. package/src/commands/elements/shot.js +9 -0
  77. package/src/commands/elements/stage.js +11 -0
  78. package/src/commands/elements.js +155 -0
  79. package/src/commands/export.js +20 -0
  80. package/src/commands/index.js +40 -0
  81. package/src/commands/motion.js +231 -0
  82. package/src/commands/objects.js +174 -0
  83. package/src/commands/project.js +53 -0
  84. package/src/commands/scene.js +81 -0
  85. package/src/commands/shared.js +19 -0
  86. package/src/commands/shot.js +103 -0
  87. package/src/commands/stage.js +17 -0
  88. package/src/commands/view.js +42 -0
  89. package/src/document-store.js +144 -0
  90. package/src/domains/cast.js +1007 -0
  91. package/src/domains/motion.js +3219 -0
  92. package/src/domains/objects.js +932 -0
  93. package/src/domains/scenes.js +817 -0
  94. package/src/domains/shots.js +404 -0
  95. package/src/domains/stage.js +93 -0
  96. package/src/dualview.jsx +113 -57
  97. package/src/facing-marks.js +3 -0
  98. package/src/first-success-guide.jsx +14 -14
  99. package/src/grid-view.js +18 -5
  100. package/src/hierarchy-model.js +24 -16
  101. package/src/hierarchy-panel.css +386 -0
  102. package/src/hierarchy-panel.jsx +150 -21
  103. package/src/{fal-motion-client.js → i2v-motion-client.js} +13 -13
  104. package/src/i2v-motion-studio.jsx +180 -0
  105. package/src/ik-camera.js +3 -0
  106. package/src/main.jsx +6 -0
  107. package/src/motion/generation.js +105 -0
  108. package/src/motion-readiness-ui.jsx +4 -4
  109. package/src/motion-readiness.js +11 -0
  110. package/src/motion-trail.js +366 -9
  111. package/src/object-gizmo.jsx +7 -1
  112. package/src/otio.js +2 -1
  113. package/src/panels/CameraPanel.jsx +60 -0
  114. package/src/panels/CharacterTransformPanel.jsx +48 -0
  115. package/src/panels/EnvironmentPanel.jsx +40 -0
  116. package/src/panels/Foldout.jsx +32 -0
  117. package/src/panels/LightPanel.jsx +24 -0
  118. package/src/panels/ObjectTransformPanel.jsx +554 -0
  119. package/src/panels/PosePanel.jsx +97 -0
  120. package/src/panels/ProjectPanel.jsx +33 -0
  121. package/src/panels/PromptBlocksPanel.jsx +442 -0
  122. package/src/panels/PropsPanel.jsx +70 -0
  123. package/src/panels/ReferenceImageField.jsx +91 -0
  124. package/src/panels/RigControlPanel.jsx +78 -0
  125. package/src/panels/RigPanel.jsx +37 -0
  126. package/src/panels/SubjectBox.jsx +47 -0
  127. package/src/panels/SubjectsPanel.jsx +39 -0
  128. package/src/panels/VideoCapturePanel.jsx +180 -0
  129. package/src/panels/details.css +1171 -0
  130. package/src/panels/motion.css +197 -0
  131. package/src/panels/pose.css +39 -0
  132. package/src/planview.jsx +60 -31
  133. package/src/posestudio.jsx +59 -4
  134. package/src/project-browser.css +1073 -0
  135. package/src/project-browser.jsx +284 -123
  136. package/src/range-pin-object-transform.js +19 -0
  137. package/src/range-pin-panel.css +518 -0
  138. package/src/range-pin-panel.jsx +344 -0
  139. package/src/result-modal.jsx +5 -5
  140. package/src/room.jsx +79 -51
  141. package/src/scene-objects.js +37 -1
  142. package/src/scenes.js +5 -0
  143. package/src/semantic-edit.js +2 -0
  144. package/src/settings-menu.jsx +39 -105
  145. package/src/shell/BottomDock.jsx +450 -0
  146. package/src/shell/DetailsSlot.jsx +513 -0
  147. package/src/shell/LibrarySlot.jsx +86 -0
  148. package/src/shell/MenuBar.jsx +601 -0
  149. package/src/shell/OutlinerSlot.jsx +50 -0
  150. package/src/shell/PreferencesDialog.jsx +538 -0
  151. package/src/shell/PreferencesSlot.jsx +40 -0
  152. package/src/shell/StatusBar.jsx +125 -0
  153. package/src/shell/StudioShell.jsx +92 -0
  154. package/src/shell/TopBar.jsx +129 -0
  155. package/src/shell/ViewportToolbar.jsx +656 -0
  156. package/src/shell/agent-glass.css +296 -0
  157. package/src/shell/dock.css +93 -0
  158. package/src/shell/glass-regions.css +985 -0
  159. package/src/shell/glass.css +687 -0
  160. package/src/shell/log-store.js +52 -0
  161. package/src/shell/mode.css +9 -0
  162. package/src/shell/preferences.css +651 -0
  163. package/src/shell/shell.css +282 -0
  164. package/src/shell/studio-shell-context.js +19 -0
  165. package/src/shell/topbar.css +435 -0
  166. package/src/shell/viewport.css +451 -0
  167. package/src/store/authored-intent.js +22 -0
  168. package/src/store/runtime-adapters.js +92 -0
  169. package/src/store/scene-stage.js +21 -0
  170. package/src/store/use-document-store.js +14 -0
  171. package/src/studio-actions.js +210 -0
  172. package/src/studio-agent-commands.js +41 -271
  173. package/src/studio-agent-context.js +39 -10
  174. package/src/studio-agent-motion.js +122 -373
  175. package/src/studio-agent-protocol.js +67 -31
  176. package/src/studio-app-binding.js +379 -0
  177. package/src/studio-contact-sheet.js +73 -0
  178. package/src/studio-elements.js +58 -51
  179. package/src/styles/themes.css +200 -0
  180. package/src/styles/tokens.css +69 -0
  181. package/src/styles.css +460 -1742
  182. package/src/theme.js +44 -0
  183. package/src/timeline-extent.js +16 -0
  184. package/src/trail-key-conflicts.js +39 -0
  185. package/src/trail-pick.js +59 -0
  186. package/src/ui.jsx +8 -8
  187. package/src/use-case-question.jsx +66 -0
  188. package/src/workflow/AgentPanel.jsx +82 -69
  189. package/src/workflow/agent-client.js +12 -2
  190. package/src/workflow/agent-panel.css +34 -32
  191. package/src/workflow/cozy-scene-node.css +2 -0
  192. package/src/workflow/workflow.css +2 -0
  193. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  194. package/tools/ardy/bridge.mjs +164 -146
  195. package/tools/ardy/visual-qa.mjs +5 -5
  196. package/tools/bench/EXP3.md +61 -0
  197. package/tools/bench/cclay_bench_extract_incam.py +125 -0
  198. package/tools/bench/cclay_bench_extract_obs.py +204 -0
  199. package/tools/bench/cclay_bench_runner.py +42 -0
  200. package/tools/bench/cube-contact.mjs +162 -0
  201. package/tools/bench/exp3.mjs +152 -0
  202. package/tools/bench/extract-bench-lib.mjs +216 -0
  203. package/tools/bench/extract-bench.mjs +302 -0
  204. package/tools/bench/fal-generate.mjs +88 -0
  205. package/tools/bench/fit/README.md +189 -0
  206. package/tools/bench/fit/camera.mjs +59 -0
  207. package/tools/bench/fit/contact.mjs +419 -0
  208. package/tools/bench/fit/footlock.mjs +234 -0
  209. package/tools/bench/fit/motion.mjs +85 -0
  210. package/tools/bench/fit/pin.mjs +27 -0
  211. package/tools/bench/fit/remote.mjs +105 -0
  212. package/tools/bench/fit-bench.mjs +126 -0
  213. package/tools/bench/fit-sanity.mjs +45 -0
  214. package/tools/bench/metrics.mjs +313 -0
  215. package/tools/bench/obs/depth.mjs +258 -0
  216. package/tools/bench/obs/extrinsics.mjs +175 -0
  217. package/tools/bench/obs/fit_mannequin_betas.py +340 -0
  218. package/tools/bench/obs/ground.mjs +294 -0
  219. package/tools/bench/obs/heading.mjs +151 -0
  220. package/tools/bench/obs/ladder.mjs +555 -0
  221. package/tools/bench/obs/mannequin-betas.json +124 -0
  222. package/tools/bench/obs/remote.mjs +83 -0
  223. package/tools/bench/obs/rest_joints.py +52 -0
  224. package/tools/bench/obs/ybot-targets.mjs +49 -0
  225. package/tools/bench/obs-bench.mjs +520 -0
  226. package/tools/bench/score.mjs +343 -0
  227. package/tools/bench/summarize.mjs +86 -0
  228. package/tools/dev/pages/privacy.html +13 -8
  229. package/tools/gt-render/browser.mjs +159 -0
  230. package/tools/gt-render/camera-math.mjs +276 -0
  231. package/tools/gt-render/page.mjs +267 -0
  232. package/tools/gt-render/render.mjs +514 -0
  233. package/tools/gt-render/scene-box.mjs +31 -0
  234. package/tools/gt-render/take-transform.mjs +87 -0
  235. package/tools/morphgs/assets/gen_truth.py +39 -0
  236. package/tools/morphgs/assets/mesh_ori_rig.txt +27 -0
  237. package/tools/morphgs/assets/playback-check.mjs +30 -0
  238. package/tools/morphgs/demo-gate.sh +26 -0
  239. package/tools/morphgs/fbx2morphgs.mjs +68 -0
  240. package/tools/morphgs/morphgs-to-cskel27.mjs +105 -0
  241. package/tools/morphgs/patches/preprocess_src-none-mode.patch +76 -0
  242. package/tools/morphgs/setup-on-cluster.sh +126 -0
  243. package/tools/qa/css-rule-usage.mjs +283 -0
  244. package/tools/qa/studio-control-count.mjs +70 -32
  245. package/tools/run-tests.mjs +134 -12
  246. package/tools/track/DESIGN.md +513 -0
  247. package/tools/track/backfill-provenance.mjs +116 -0
  248. package/tools/track/budget.mjs +8 -0
  249. package/tools/track/check-rig.mjs +209 -0
  250. package/tools/track/diagnostics.schema.json +60 -0
  251. package/tools/track/export-rig.mjs +256 -0
  252. package/tools/track/fallback.mjs +7 -0
  253. package/tools/track/fk-parity-fixture.mjs +161 -0
  254. package/tools/track/gate.mjs +349 -0
  255. package/tools/track/masks.mjs +318 -0
  256. package/tools/track/metrics.mjs +201 -0
  257. package/tools/track/publish-obs.mjs +108 -0
  258. package/tools/track/py/check_env.py +61 -0
  259. package/tools/track/py/eval_lr.py +229 -0
  260. package/tools/track/py/lr_viterbi.py +294 -0
  261. package/tools/track/py/masks.py +405 -0
  262. package/tools/track/py/objective.py +382 -0
  263. package/tools/track/py/rig.py +508 -0
  264. package/tools/track/py/scene.py +326 -0
  265. package/tools/track/py/test_joint_indices.py +123 -0
  266. package/tools/track/py/test_lr_viterbi.py +195 -0
  267. package/tools/track/py/test_masks.py +229 -0
  268. package/tools/track/py/test_rig.py +288 -0
  269. package/tools/track/py/test_scene.py +354 -0
  270. package/tools/track/py/test_track.py +328 -0
  271. package/tools/track/py/track.py +411 -0
  272. package/tools/track/remote.mjs +140 -0
  273. package/tools/track/rig-dump.mjs +227 -0
  274. package/tools/track/run-box-tests.mjs +14 -0
  275. package/tools/track/run-box.mjs +168 -0
  276. package/tools/track/setup-box.sh +59 -0
  277. package/tools/track/study-2d.mjs +728 -0
  278. package/dist/assets/analytics-B1hnH66c.js +0 -1
  279. package/dist/assets/app-BWusbqgO.js +0 -4861
  280. package/dist/assets/app-qKDo4PBX.css +0 -1
  281. package/dist/assets/shot-prompt-CHUtw6af.js +0 -17
  282. package/dist/assets/shot-prompt-C_g2BHVc.css +0 -1
  283. package/dist/assets/workflow-CVwzMhz2.js +0 -179
  284. package/dist/assets/workflow-DsudxKHF.css +0 -1
  285. package/src/fal-motion-studio.jsx +0 -180
  286. package/src/scene-history.js +0 -129
@@ -1,34 +1,26 @@
1
- // Private Studio motion candidates. No renderer installation, App callbacks or
2
- // telemetry: the editor owns the atomic domain commit and its shared journal.
1
+ // Read-only verification of installed takes on isolated rig clones.
2
+ // Generation, repair, publication and history belong to the motion domain.
3
3
  import * as THREE from 'three';
4
4
  import { clone as cloneSkeleton } from 'three/examples/jsm/utils/SkeletonUtils.js';
5
- import { StudioProtocolError, StudioSchemas, validateStudioSchema, validateStudioIdentity, validateTargetGuard, validateFrameRange, validateReceipt, freezeStudioData } from './studio-agent-protocol.js';
6
- import { loadMotionFromUrl, characterScaleFor } from './ardy/npz.js';
7
- import { retimeMotion } from './ardy/retime.js';
8
- import { applyMotionCalibration, normalizeMotionCalibration } from './ardy/motion-calibration.js';
9
- import { applySupportRise, autoRoofDrop, applyAutoFall } from './ardy/root-drop.js';
10
- import { createMotionEdit } from './ardy/motion-edit.js';
5
+ import { StudioProtocolError, freezeStudioData } from './studio-agent-protocol.js';
11
6
  import { applyMotionFrame } from './ardy/playback.js';
12
7
  import { resolveIkRig, createIkState, ikEvaluate, findBone, hasBindPose } from './ardy/ik.js';
13
- import { PHYSICS_LIMITS, SUPPORT_SITES, createSupportSampler, copyPhysicsKeys, physicsKeyStamp, supportIntervals, physicsMetrics, reviewAutoPhysics } from './ardy/physics-review.js';
8
+ import { PHYSICS_LIMITS, createSupportSampler, copyPhysicsKeys, supportIntervals, physicsMetrics } from './ardy/physics-review.js';
14
9
  import { createDynamicsSampler, supportDiagnostics } from './ardy/physics-support.js';
15
10
  import { computeCenterOfMass } from './ardy/auto-physics.js';
16
- import { buildCollisionCapsules, detectPenetrations, supportsCollisionCleanup, fixCollisionsRange } from './ardy/fix-collisions.js';
11
+ import { buildCollisionCapsules, detectPenetrations, supportsCollisionCleanup } from './ardy/fix-collisions.js';
17
12
  import { collisionBlockers, blockerSummary } from './ardy/collision-blockers.js';
18
13
  import { createGroundSampler } from './ardy/ground.js';
19
- import { supportHeightForObject, OBJECT_LIBRARY } from './scene-objects.js';
14
+ import { OBJECT_LIBRARY } from './scene-objects.js';
20
15
  import { objectTransformAt } from './object-path.js';
21
16
  import { sampleAt } from './sample-at.js';
22
17
 
23
18
  const PROFILE = 'studio-motion-v1', BLEND = 6;
24
19
  const fail = (code, message) => { throw new StudioProtocolError(code, message); };
25
- const equal = (a, b) => JSON.stringify(a) === JSON.stringify(b);
26
- const id = value => validateStudioSchema(StudioSchemas.TargetGuard.properties.targetId, value);
27
20
  const copyLayer = state => ({ ...createIkState(), keys: copyPhysicsKeys(state?.keys ?? new Map()), tracked: new Set(state?.tracked ?? []) });
28
21
  const poseOf = rig => { const result = []; rig.traverse(n => { if (n.isBone) result.push({ bone: n, p: n.position.clone(), q: n.quaternion.clone(), s: n.scale.clone() }); }); return result; };
29
22
  const restore = pose => { for (const { bone, p, q, s } of pose) { bone.position.copy(p); bone.quaternion.copy(q); bone.scale.copy(s); } };
30
23
  const poseValues = pose => pose.map(({ bone }) => [...bone.position.toArray(), ...bone.quaternion.toArray(), ...bone.scale.toArray()]);
31
- const environmentKey = env => JSON.stringify([validateStudioIdentity(env.host), env.physicsRevision, { model: env.floor.model, y: env.floor.y }]);
32
24
  const BASE_LIMITATIONS = ['discrete-integer-24fps-frames', 'calibrated-capsule-and-upright-box-proxies', 'external-torso-head-and-other-cast-fingers-excluded', 'rest-overlap-calibration-and-4mm-actionable-depth', 'centroidal-support-not-biomechanical-certification', 'semantic-and-visual-review-unavailable'];
33
25
 
34
26
  function isolatedRig(source) {
@@ -50,7 +42,7 @@ function isolatedRig(source) {
50
42
  }
51
43
  function disposeRig(value) {
52
44
  // Geometry/material/texture assets are borrowed read-only. Only cloned
53
- // skeleton palettes and the private hierarchy belong to this candidate.
45
+ // skeleton palettes and the private hierarchy belong to this evaluation.
54
46
  const skeletons = new Set(); value.rig.traverse(n => { if (n.isSkinnedMesh) skeletons.add(n.skeleton); });
55
47
  for (const skeleton of skeletons) skeleton.dispose();
56
48
  value.parent.remove(value.rig);
@@ -71,370 +63,127 @@ function readSample(evaluator, groundAt) {
71
63
  const ground = Object.fromEntries(Object.entries(support).map(([key, p]) => [key, groundAt(p.point.x, p.point.z, p.position.y + .001)]));
72
64
  return { support, toes, knees, ground, root: evaluator.fkJoints.get('hips').bone.getWorldPosition(new THREE.Vector3()), com: computeCenterOfMass(evaluator.rig), dynamics: evaluator.dynamics() };
73
65
  }
74
- function validateClip(clip, frameCount) {
75
- if (clip.fps !== 24 || clip.frames !== frameCount) fail('VERIFICATION_FAILED', 'Retimed clip and compiled schedule disagree.');
76
- for (const [key, width] of [['rotMats', 243], ['rootPos', 3], ['posedJoints', 81]]) {
77
- if (clip[key]?.length !== frameCount * width || !clip[key].every(Number.isFinite)) fail('VERIFICATION_FAILED', 'Invalid decoded motion structure.');
78
- }
66
+ const nextTask = () => new Promise(resolve => {
67
+ const channel = new MessageChannel(); channel.port1.onmessage = () => { channel.port1.close(); channel.port2.close(); resolve(); }; channel.port2.postMessage(0);
68
+ });
69
+ const verificationBudget = (frames, ms) => (ms ?? 60000) * Math.max(1, frames / 48) * 2;
70
+ function checkEnvironment(env) {
71
+ if (!Number.isSafeInteger(env.physicsRevision) || env.physicsRevision < 0 || !Number.isFinite(env.floor?.y)) fail('TARGET_NOT_READY', 'Authoritative physics revision and floor are required.');
72
+ return env;
79
73
  }
80
- function scheduleOf(value) {
81
- if (value?.fps !== 24 || !Number.isSafeInteger(value.frameCount) || value.frameCount < 48 || value.frameCount > 1440 || value.durationSeconds !== value.frameCount / 24 || !Array.isArray(value.blocks) || !value.blocks.length) fail('INVALID_ARGUMENT', 'A compiled 2-60 second production schedule is required.');
82
- let end = 0;
83
- for (const block of value.blocks) {
84
- validateFrameRange({ startFrame: block.startFrame, endFrameExclusive: block.endFrameExclusive });
85
- if (block.startFrame !== end || block.endFrameExclusive - end > 120) fail('INVALID_ARGUMENT', 'Schedule must be contiguous with bounded blocks.');
86
- validateStudioSchema(StudioSchemas.ReceiptVariants.installed.properties.installed.properties.blocks.items.properties.sourceBeat, block.sourceBeat);
87
- end = block.endFrameExclusive;
74
+ const copyEnvironment = env => ({ ...env, host: structuredClone(env.host), floor: structuredClone(env.floor), objects: structuredClone(env.objects), cast: undefined });
75
+ /** Private evaluators for the other visible cast, pushed one by one so a
76
+ * refused rig leaves every evaluator made before it disposable. */
77
+ function stageCast(env, characterId, cast) {
78
+ for (const member of env.cast) {
79
+ if (member.character.id === characterId || member.character.hidden) continue;
80
+ // Unsupported bystanders remain explicit missing coverage, never vanish
81
+ // into characterBlockers' otherwise legitimate best-effort skip path.
82
+ const entry = { character: structuredClone(member.character), motion: member.motion ? structuredClone(member.motion) : null, layer: copyLayer(member.ikState), evaluator: null };
83
+ cast.push(entry);
84
+ if (member.rig && hasBindPose(member.rig) && supportsCollisionCleanup(member.rig)) entry.evaluator = isolatedRig(member.rig);
88
85
  }
89
- if (end !== value.frameCount) fail('INVALID_ARGUMENT', 'Schedule does not span the whole clip.');
90
- return freezeStudioData(structuredClone(value));
91
86
  }
92
87
 
93
- /** Required ports (all reads synchronous):
94
- * readTarget(binding) -> {guard, character, rig, ikState, calibration?,
95
- * protectedFrames?, preserveAuthoredMotion?}; rig is that character's current
96
- * world-space rig with poseBind. readEnvironment() -> {host, physicsRevision,
97
- * floor:{model:'flat',y}, objects, cast:[{character,rig,motion,ikState}], frameCount}.
98
- * Character paths not baked into a supplied rig/clip require poseCast(evaluator,
99
- * entry,frame), which may touch ONLY that private evaluator. See contract.md.
100
- * journal is task 2's per-document journal. commit(payload) is task 5's single
101
- * synchronous atomic owner, returning/journaling the actual installed receipt.
102
- * loadArtifact defaults to the real bounded NPZ fetch/decode; it never installs.
103
- */
104
- export function createStudioMotionCandidates(ports) {
105
- const candidates = new Map(), admissions = new Map();
106
- const now = ports.now ?? Date.now, newId = ports.newId ?? (() => crypto.randomUUID());
107
- const yieldTask = ports.yieldTask ?? (() => new Promise(resolve => {
108
- const channel = new MessageChannel(); channel.port1.onmessage = () => { channel.port1.close(); channel.port2.close(); resolve(); }; channel.port2.postMessage(0);
109
- }));
110
- const ttlMs = ports.ttlMs ?? 600000;
111
- let disposed = false;
112
- function checkBinding(request) {
113
- id(request.commandId);
114
- const b = request.binding;
115
- const expected = { ...validateStudioIdentity(b.host), targetId: id(b.characterId), token: id(b.targetToken) };
116
- const target = ports.readTarget(b);
117
- if (!equal(validateStudioIdentity(ports.readEnvironment().host), b.host)) fail('STALE_SCENE', 'Candidate document is no longer current.');
118
- validateTargetGuard(expected, target?.guard);
119
- return target;
120
- }
121
- function fence(candidate) {
122
- if (disposed || candidate.cancelled || ports.isCancelled?.(candidate.request.commandId)) fail('CANCELLED', 'Candidate installation permission was revoked.');
123
- if (now() - candidate.createdAt >= ttlMs) fail('STALE_TARGET', 'Private candidate expired.');
124
- checkBinding(candidate.request);
125
- }
126
- function release(candidate) {
127
- if (candidate.released) return;
128
- candidate.released = true;
129
- candidate.controller.abort();
130
- if (candidate.evaluator) disposeRig(candidate.evaluator);
131
- for (const member of candidate.cast ?? []) if (member.evaluator) disposeRig(member.evaluator);
132
- candidate.cache = null; candidate.autoCache.value = null; candidate.evidence = null;
133
- candidate.evaluator = null; candidate.cast = []; candidate.motion = null; candidate.layer = null;
134
- candidates.delete(candidate.candidateId);
135
- }
136
- function rejection(request, error, phase) {
137
- const code = error instanceof StudioProtocolError ? error.code : 'VERIFICATION_FAILED';
138
- return validateReceipt({ ok: false, commandId: request.commandId, host: request.binding.host, code, phase,
139
- affectedIds: [request.binding.characterId], expectedTargets: [{ ...request.binding.host, targetId: request.binding.characterId, token: request.binding.targetToken }], currentTargets: [], mutated: false,
140
- preserved: { authoredState: 'unchanged' }, recovery: { action: 'new_intent', retryAllowed: false } });
141
- }
142
- async function operation(request, phase, work) {
143
- let candidate = candidates.get(request.candidateId);
144
- try {
145
- if (phase !== 'prepare') {
146
- if (!candidate || candidate.request.commandId !== request.commandId || !equal(candidate.request.binding, request.binding)) fail('STALE_TARGET', 'Candidate does not belong to this command.');
147
- fence(candidate);
148
- if (request.candidateRevision !== candidate.revision) fail('STALE_TARGET', 'Candidate revision changed.');
149
- if (candidate.busy) fail('TARGET_BUSY', 'Candidate evaluation is already in progress.');
150
- candidate.busy = true;
151
- }
152
- return await work(candidate);
153
- } catch (error) {
154
- candidate ??= [...candidates.values()].find(c => c.request.commandId === request.commandId);
155
- // Foreign requests and concurrent calls must not dispose another owner.
156
- if (candidate && candidate.request.commandId === request.commandId && equal(candidate.request.binding, request.binding) && error.code !== 'TARGET_BUSY') release(candidate);
157
- const receipt = rejection(request, error, phase);
158
- if (ports.journal.get(request.commandId) == null && error.code !== 'TARGET_BUSY') ports.journal.record(receipt);
159
- return receipt;
160
- } finally { if (candidate) candidate.busy = false; }
161
- }
162
- const summary = c => ({ candidateId: c.candidateId, candidateRevision: c.revision, targetToken: c.request.binding.targetToken, physicsRevision: c.env.physicsRevision, structurallyValid: true });
163
- function captureEnvironment(c) {
164
- const env = ports.readEnvironment();
165
- if (!Number.isSafeInteger(env.physicsRevision) || env.physicsRevision < 0 || !Number.isFinite(env.floor?.y)) fail('TARGET_NOT_READY', 'Authoritative physics revision and floor are required.');
166
- const key = environmentKey(env);
167
- if (c.envKey === key) return;
168
- if (c.envKey) {
169
- if (c.revalidatedEnvironment) fail('STALE_ENVIRONMENT', 'The environment changed more than once.');
170
- c.revalidatedEnvironment = true;
171
- }
172
- for (const member of c.cast) if (member.evaluator) disposeRig(member.evaluator);
173
- c.cast = [];
174
- c.env = { ...env, host: structuredClone(env.host), floor: structuredClone(env.floor), objects: structuredClone(env.objects), cast: undefined };
175
- c.envKey = key; c.cache = null; c.autoCache.value = null;
176
- for (const member of env.cast) {
177
- if (member.character.id === c.request.binding.characterId || member.character.hidden) continue;
178
- // Unsupported bystanders remain explicit missing coverage, never vanish
179
- // into characterBlockers' otherwise legitimate best-effort skip path.
180
- const entry = { character: structuredClone(member.character), motion: member.motion ? structuredClone(member.motion) : null, layer: copyLayer(member.ikState), evaluator: null };
181
- c.cast.push(entry);
182
- if (member.rig && hasBindPose(member.rig) && supportsCollisionCleanup(member.rig)) entry.evaluator = isolatedRig(member.rig);
183
- }
88
+ /* One take under evaluation, `t`: {evaluator, motion, env, cast, characterId,
89
+ * character, protectedFrames, range, poseCast?, yieldTask, checkpoint}. */
90
+ function blockersAt(t, frame) {
91
+ t.checkpoint();
92
+ const rigs = {};
93
+ for (const member of t.cast) {
94
+ if (!member.evaluator) continue;
95
+ poseFrame(member.evaluator, member.motion, member.layer, frame);
96
+ t.poseCast?.(member.evaluator, member, frame);
97
+ member.evaluator.parent.updateMatrixWorld(true);
98
+ rigs[member.character.id] = member.evaluator.rig;
184
99
  }
185
- function checkpoint(c) {
186
- fence(c);
187
- if (environmentKey(ports.readEnvironment()) !== c.envKey) fail('STALE_ENVIRONMENT', 'Physical inputs changed during evaluation.');
188
- if (now() > c.deadline) fail('VERIFICATION_FAILED', 'Private verification/repair budget exceeded.');
189
- }
190
- function blockersAt(c, frame) {
191
- checkpoint(c);
192
- const rigs = {};
193
- for (const member of c.cast) {
194
- if (!member.evaluator) continue;
195
- poseFrame(member.evaluator, member.motion, member.layer, frame);
196
- ports.poseCast?.(member.evaluator, member, frame);
197
- member.evaluator.parent.updateMatrixWorld(true);
198
- rigs[member.character.id] = member.evaluator.rig;
199
- }
200
- return collisionBlockers({ rigs, activeId: c.request.binding.characterId, sceneObjects: c.env.objects, library: OBJECT_LIBRARY, frame, take: { frameCount: c.env.frameCount, fps: 24 } });
201
- }
202
- async function samples(c, layer) {
203
- const rows = [], poses = [], collisions = [], blockers = [];
204
- for (let frame = 0; frame < c.motion.frames; frame++) {
205
- checkpoint(c); poseFrame(c.evaluator, c.motion, layer, frame);
206
- const shapes = blockersAt(c, frame);
207
- const objects = c.env.objects.map(object => {
208
- const at = objectTransformAt(object, frame, { frameCount: c.env.frameCount, fps: 24 });
209
- return at ? { ...object, x: at.x, y: at.y, z: at.z, rot: at.rot ?? object.rot } : object;
210
- });
211
- rows.push(readSample(c.evaluator, createGroundSampler(objects, { floorY: c.env.floor.y })));
212
- poses.push(poseValues(c.evaluator.rest));
213
- const capsules = buildCollisionCapsules(c.evaluator.rig);
214
- collisions.push(detectPenetrations(capsules, { blockers: shapes }).map(p => ({ a: p.a.def.id, b: p.b.def?.id ?? p.b.id, depth: p.depth })));
215
- blockers.push(blockerSummary(shapes));
216
- if (frame % 12 === 11) { await yieldTask(); checkpoint(c); }
217
- }
218
- const contacts = supportIntervals(rows, 24, [], c.env.floor.y);
219
- return { rows, poses, collisions, blockers, contacts, metrics: physicsMetrics(rows, contacts.masks, 24, c.env.floor.y), support: supportDiagnostics(rows, 24, c.env.floor.y) };
100
+ const cast = t.cast.map((member) => member.character).filter(Boolean);
101
+ return collisionBlockers({ rigs, activeId: t.characterId, characterIds: cast, sceneObjects: t.env.objects, library: OBJECT_LIBRARY, frame, take: { frameCount: t.env.frameCount, fps: 24 } });
102
+ }
103
+ async function samples(t, layer) {
104
+ const rows = [], poses = [], collisions = [], blockers = [];
105
+ const cast = t.cast.map((member) => member.character).filter(Boolean);
106
+ for (let frame = t.range.startFrame; frame < t.range.endFrameExclusive; frame++) {
107
+ t.checkpoint(); poseFrame(t.evaluator, t.motion, layer, frame);
108
+ const shapes = blockersAt(t, frame);
109
+ const objects = t.env.objects.map(object => {
110
+ const at = objectTransformAt(object, frame, { frameCount: t.env.frameCount, fps: 24 });
111
+ return at ? { ...object, x: at.x, y: at.y, z: at.z, rot: at.rot ?? object.rot } : object;
112
+ });
113
+ rows.push(readSample(t.evaluator, createGroundSampler(objects, { floorY: t.env.floor.y, characters: cast })));
114
+ poses.push(poseValues(t.evaluator.rest));
115
+ const capsules = buildCollisionCapsules(t.evaluator.rig);
116
+ collisions.push(detectPenetrations(capsules, { blockers: shapes }).map(p => ({ a: p.a.def.id, b: p.b.def?.id ?? p.b.id, depth: p.depth })));
117
+ blockers.push(blockerSummary(shapes));
118
+ if (frame % 12 === 11) { await t.yieldTask(); t.checkpoint(); }
220
119
  }
221
- async function evaluate(c) {
222
- captureEnvironment(c);
223
- c.deadline = now() + (ports.verificationMs ?? 60000);
224
- if (!c.cache) c.cache = await samples(c, copyLayer());
225
- const before = c.cache, after = await samples(c, c.layer), m = after.metrics;
226
- const continuityRegressed = m.kneeStep > Math.max(before.metrics.kneeStep + 2, 12)
227
- || m.kneeAcceleration > Math.max(1e-6, before.metrics.kneeAcceleration * 1.1)
228
- || m.rootAcceleration > Math.max(1e-6, before.metrics.rootAcceleration * 1.1);
229
- let protectedPoseError = 0;
230
- for (const f of c.protectedFrames) for (let b = 0; b < before.poses[f].length; b++) for (let i = 0; i < before.poses[f][b].length; i++) protectedPoseError = Math.max(protectedPoseError, Math.abs(before.poses[f][b][i] - after.poses[f][b][i]));
231
- const supportedCollisionFrames = after.collisions.filter(p => p.length).length;
232
- const elevatedFrames = after.rows.flatMap((row, frame) => Object.values(row.ground).some(y => y !== c.env.floor.y) ? [frame] : []);
233
- const unsupportedObjects = c.env.objects.filter(o => o.attach || o.rotX || o.rotZ);
234
- const missingCast = c.cast.filter(m => !m.evaluator || ((m.character.layer?.waypoints?.length ?? 0) > 0 && !ports.poseCast));
235
- const measuredSupportFrames = after.support.frames.filter(f => f.measured).length;
236
- const flat = c.env.floor.model === 'flat' && elevatedFrames.length === 0 && Math.abs(c.character.y ?? 0) < 1e-8;
237
- const contactDefects = m.penetration > PHYSICS_LIMITS.floor || m.slide > PHYSICS_LIMITS.slide || m.float > PHYSICS_LIMITS.float || after.support.unsupportedFrames > 0;
238
- const coverageComplete = m.surfaceMeasured && measuredSupportFrames === c.motion.frames && after.contacts.spans.length > 0 && !missingCast.length && !unsupportedObjects.length;
239
- const verified = coverageComplete && flat && !contactDefects && !after.support.unresolved.length && !supportedCollisionFrames && !continuityRegressed && protectedPoseError <= 1e-8;
240
- const limitations = [...BASE_LIMITATIONS];
241
- if (!m.surfaceMeasured || measuredSupportFrames !== c.motion.frames) limitations.push('skin-or-dynamics-coverage-unavailable');
242
- if (!after.contacts.spans.length) limitations.push('no-reliable-inferred-contact-spans');
243
- if (!flat) limitations.push('elevated-moving-or-nonflat-support-unsupported');
244
- if (missingCast.length) limitations.push('other-cast-evaluation-incomplete');
245
- if (unsupportedObjects.length) limitations.push('attached-or-tilted-object-proxies-unsupported');
246
- const repairable = !c.sealed && !c.revalidatedEnvironment && flat && m.surfaceMeasured && !missingCast.length && !unsupportedObjects.length
247
- && ((!c.autoAttempted && contactDefects) || (!c.collisionAttempted && supportedCollisionFrames > 0));
248
- const result = { ...summary(c), verificationId: newId(), profile: PROFILE, status: verified ? 'verified' : 'unverified', repairable,
249
- range: { startFrame: 0, endFrameExclusive: c.motion.frames }, evaluatedFrames: after.rows.length,
250
- coverage: { sourceFrames: before.rows.length, candidateFrames: after.rows.length, measuredSupportFrames, contactSpans: after.contacts.spans.length,
251
- collisionFrames: after.collisions.length, otherCastIds: c.cast.map(m => m.character.id), pathObjectIds: c.env.objects.filter(o => o.path).map(o => o.id), elevatedFrames },
252
- metrics: { surfaceMeasured: m.surfaceMeasured, maxFloorPenetrationM: m.penetration, maxContactSlipM: m.slide, maxContactFloatM: m.float,
253
- unsupportedFrames: after.support.unsupportedFrames, supportedCollisionFrames, continuityRegressed, protectedPoseError,
254
- kneeAcceleration: m.kneeAcceleration, rootAcceleration: m.rootAcceleration, supportForceResidual: after.support.forceResidual, supportMomentResidual: after.support.momentResidual },
255
- limitations, visualRefs: [], semanticStatus: 'unavailable', repairs: repairCounts(c) };
256
- if (verified || !repairable || c.collisionAttempted) c.sealed = true;
257
- c.evidence = { before, after }; c.verification = freezeStudioData(result); c.verifiedStamp = physicsKeyStamp(c.layer.keys);
258
- checkpoint(c);
259
- return c.verification;
120
+ const contacts = supportIntervals(rows, 24, [], t.env.floor.y);
121
+ return { rows, poses, collisions, blockers, contacts, metrics: physicsMetrics(rows, contacts.masks, 24, t.env.floor.y), support: supportDiagnostics(rows, 24, t.env.floor.y) };
122
+ }
123
+ /** The verdict on one layer's samples (`after`) against the bare take's
124
+ * (`before`): status inputs, coverage, metrics and limitations. */
125
+ function judge(t, before, after) {
126
+ const m = after.metrics;
127
+ const continuityRegressed = m.kneeStep > Math.max(before.metrics.kneeStep + 2, 12)
128
+ || m.kneeAcceleration > Math.max(1e-6, before.metrics.kneeAcceleration * 1.1)
129
+ || m.rootAcceleration > Math.max(1e-6, before.metrics.rootAcceleration * 1.1);
130
+ let protectedPoseError = 0;
131
+ for (const frame of t.protectedFrames) {
132
+ const f = frame - t.range.startFrame;
133
+ if (f < 0 || f >= before.poses.length) continue;
134
+ for (let b = 0; b < before.poses[f].length; b++) for (let i = 0; i < before.poses[f][b].length; i++) protectedPoseError = Math.max(protectedPoseError, Math.abs(before.poses[f][b][i] - after.poses[f][b][i]));
260
135
  }
261
- const repairCounts = c => ({ autoPhysicsInvocations: c.autoInvocations, fixCollisionsInvocations: c.collisionInvocations, remaining: c.sealed ? 0 : Number(!c.autoAttempted) + Number(!c.collisionAttempted) });
262
- function receiptVerification(c) {
263
- const v = c.verification, m = v.metrics;
264
- return { id: v.verificationId, status: v.status, profile: PROFILE, range: v.range, evaluatedFrames: v.evaluatedFrames, physicsRevision: v.physicsRevision,
265
- limitations: v.limitations, surfaceMeasured: m.surfaceMeasured, maxFloorPenetrationM: m.maxFloorPenetrationM, maxContactSlipM: m.maxContactSlipM, maxContactFloatM: m.maxContactFloatM,
266
- unsupportedFrames: m.unsupportedFrames, supportedCollisionFrames: m.supportedCollisionFrames, continuityRegressed: m.continuityRegressed, semanticStatus: 'unavailable' };
136
+ const supportedCollisionFrames = after.collisions.filter(p => p.length).length;
137
+ const elevatedFrames = after.rows.flatMap((row, f) => Object.values(row.ground).some(y => y !== t.env.floor.y) ? [t.range.startFrame + f] : []);
138
+ const unsupportedObjects = t.env.objects.filter(o => o.attach || o.rotX || o.rotZ);
139
+ const missingCast = t.cast.filter(m => !m.evaluator || ((m.character.layer?.waypoints?.length ?? 0) > 0 && !t.poseCast));
140
+ const measuredSupportFrames = after.support.frames.filter(f => f.measured).length;
141
+ const flat = t.env.floor.model === 'flat' && elevatedFrames.length === 0 && Math.abs(t.character.y ?? 0) < 1e-8;
142
+ const contactDefects = m.penetration > PHYSICS_LIMITS.floor || m.slide > PHYSICS_LIMITS.slide || m.float > PHYSICS_LIMITS.float || after.support.unsupportedFrames > 0;
143
+ const coverageComplete = m.surfaceMeasured && measuredSupportFrames === after.rows.length && after.contacts.spans.length > 0 && !missingCast.length && !unsupportedObjects.length;
144
+ const verified = coverageComplete && flat && !contactDefects && !after.support.unresolved.length && !supportedCollisionFrames && !continuityRegressed && protectedPoseError <= 1e-8;
145
+ const limitations = [...BASE_LIMITATIONS];
146
+ if (!m.surfaceMeasured || measuredSupportFrames !== after.rows.length) limitations.push('skin-or-dynamics-coverage-unavailable');
147
+ if (!after.contacts.spans.length) limitations.push('no-reliable-inferred-contact-spans');
148
+ if (!flat) limitations.push('elevated-moving-or-nonflat-support-unsupported');
149
+ if (missingCast.length) limitations.push('other-cast-evaluation-incomplete');
150
+ if (unsupportedObjects.length) limitations.push('attached-or-tilted-object-proxies-unsupported');
151
+ return { verified, flat, contactDefects, missingCast, unsupportedObjects, supportedCollisionFrames, limitations,
152
+ range: { ...t.range }, evaluatedFrames: after.rows.length,
153
+ coverage: { sourceFrames: before.rows.length, candidateFrames: after.rows.length, measuredSupportFrames, contactSpans: after.contacts.spans.length,
154
+ collisionFrames: after.collisions.length, otherCastIds: t.cast.map(m => m.character.id), pathObjectIds: t.env.objects.filter(o => o.path).map(o => o.id), elevatedFrames },
155
+ metrics: { surfaceMeasured: m.surfaceMeasured, maxFloorPenetrationM: m.penetration, maxContactSlipM: m.slide, maxContactFloatM: m.float,
156
+ unsupportedFrames: after.support.unsupportedFrames, supportedCollisionFrames, continuityRegressed, protectedPoseError,
157
+ kneeAcceleration: m.kneeAcceleration, rootAcceleration: m.rootAcceleration, supportForceResidual: after.support.forceResidual, supportMomentResidual: after.support.momentResidual } };
158
+ }
159
+ /** The verification a receipt carries, from a full verification result. */
160
+ function receiptVerification(v) {
161
+ const m = v.metrics;
162
+ return { id: v.verificationId, status: v.status, profile: PROFILE, range: v.range, evaluatedFrames: v.evaluatedFrames, physicsRevision: v.physicsRevision,
163
+ limitations: v.limitations, surfaceMeasured: m.surfaceMeasured, maxFloorPenetrationM: m.maxFloorPenetrationM, maxContactSlipM: m.maxContactSlipM, maxContactFloatM: m.maxContactFloatM,
164
+ unsupportedFrames: m.unsupportedFrames, supportedCollisionFrames: m.supportedCollisionFrames, continuityRegressed: m.continuityRegressed, semanticStatus: 'unavailable' };
165
+ }
166
+ /** Motion verification of an installed take, on a private rig clone over
167
+ * `range` (default and upper bound: the whole take). target: {character, rig,
168
+ * motion, ikState?, protectedFrames?}; environment has readEnvironment()'s
169
+ * shape. Returns the receipt verification shape tagged with the character id. */
170
+ export async function verifyInstalledTake({ target, environment, range, poseCast, yieldTask = nextTask, now = Date.now, verificationMs, newId = () => crypto.randomUUID() }) {
171
+ const motion = target?.motion;
172
+ if (!motion?.frames) fail('TARGET_NOT_READY', 'The character has no installed take.');
173
+ const env = copyEnvironment(checkEnvironment(environment));
174
+ const startFrame = range?.startFrame ?? 0, endFrameExclusive = Math.min(range?.endFrameExclusive ?? motion.frames, motion.frames);
175
+ if (startFrame >= endFrameExclusive) fail('INVALID_RANGE', 'The range lies outside the installed take.');
176
+ const deadline = now() + verificationBudget(endFrameExclusive - startFrame, verificationMs);
177
+ const t = { evaluator: isolatedRig(target.rig), motion, env, cast: [], characterId: target.character.id, character: target.character,
178
+ protectedFrames: target.protectedFrames ?? [], range: { startFrame, endFrameExclusive }, poseCast, yieldTask,
179
+ checkpoint: () => { if (now() > deadline) fail('VERIFICATION_FAILED', 'Private verification budget exceeded.'); } };
180
+ try {
181
+ stageCast(environment, t.characterId, t.cast);
182
+ const before = await samples(t, copyLayer()), after = await samples(t, copyLayer(target.ikState)), j = judge(t, before, after);
183
+ return freezeStudioData({ characterId: t.characterId, ...receiptVerification({ verificationId: newId(), status: j.verified ? 'verified' : 'unverified',
184
+ range: j.range, evaluatedFrames: j.evaluatedFrames, physicsRevision: env.physicsRevision, limitations: j.limitations, metrics: j.metrics }) });
185
+ } finally {
186
+ disposeRig(t.evaluator);
187
+ for (const member of t.cast) if (member.evaluator) disposeRig(member.evaluator);
267
188
  }
268
- const api = {
269
- prepare_motion_install(request) {
270
- // Identical preparation replay shares one promise and one private rig.
271
- checkBinding(request);
272
- const signature = JSON.stringify(request);
273
- const existing = admissions.get(request.commandId);
274
- if (existing) { if (existing.signature !== signature) fail('INVALID_ARGUMENT', 'Motion command identity cannot change.'); return existing.promise; }
275
- ports.journal.begin(request.commandId, signature);
276
- const promise = operation(request, 'prepare', async () => {
277
- if (disposed) fail('CANCELLED', 'Candidate owner disposed.');
278
- const target = checkBinding(request), schedule = scheduleOf(request.schedule);
279
- if (target.preserveAuthoredMotion) fail('CAPABILITY_MISSING', 'Unconstrained generation cannot preserve authored motion constraints.');
280
- if (request.stagingPolicy !== 'preserve-target-anchor' || request.artifactId !== request.artifact?.artifactId) fail('INVALID_ARGUMENT', 'Unsupported staging policy or artifact identity.');
281
- id(request.artifactId); id(request.jobId);
282
- const c = { request: structuredClone(request), candidateId: newId(), revision: 1, createdAt: now(), controller: new AbortController(), cast: [], autoCache: { value: null },
283
- character: structuredClone(target.character), schedule, protectedFrames: [...new Set(target.protectedFrames ?? [])], autoAttempted: false, collisionAttempted: false, autoInvocations: 0, collisionInvocations: 0, sealed: false, busy: true };
284
- candidates.set(c.candidateId, c);
285
- for (const frame of c.protectedFrames) if (!Number.isSafeInteger(frame) || frame < 0 || frame >= schedule.frameCount) fail('INVALID_RANGE', 'Protected frame lies outside the candidate.');
286
- c.evaluator = isolatedRig(target.rig);
287
- captureEnvironment(c);
288
- const deadline = now() + (ports.preparationMs ?? 30000);
289
- const signal = AbortSignal.any([c.controller.signal, AbortSignal.timeout(ports.preparationMs ?? 30000)]);
290
- const decoded = structuredClone(await (ports.loadArtifact ?? ((artifact, options) => loadMotionFromUrl(artifact.url, options)))(request.artifact, { signal }));
291
- fence(c); signal.throwIfAborted();
292
- if (now() > deadline) fail('VERIFICATION_FAILED', 'Private preparation budget exceeded.');
293
- const calibration = normalizeMotionCalibration(target.calibration);
294
- // A captured take needs the same explicit calibration/stature policy.
295
- // Canonical generated takes preserve the bound character's stature.
296
- if ((decoded.boneScale || decoded.personScale !== 1) && (!target.calibration || Math.abs(characterScaleFor(decoded) - c.character.scale) > 1e-8)) fail('CAPABILITY_MISSING', 'Captured motion requires matching target calibration and stature.');
297
- c.calibration = calibration;
298
- let motion = applyMotionCalibration(retimeMotion(decoded, 24), { ...calibration, yawDeg: 0, offsetX: 0, offsetZ: 0 }).motion;
299
- validateClip(motion, schedule.frameCount);
300
- const supports = c.env.objects.filter(o => !o.path && !o.attach && !o.rotX && !o.rotZ).map(o => ({ x: o.x, z: o.z, rotDeg: o.rot ?? 0,
301
- supportY: (o.y ?? 0) + supportHeightForObject(o) * (o.scaleY ?? 1), topY: (o.y ?? 0) + supportHeightForObject(o) * (o.scaleY ?? 1), width: o.footprint.width * o.scaleX, depth: o.footprint.depth * o.scaleZ }));
302
- const anchor = { x: c.character.x, y: c.character.y ?? 0, z: c.character.z, rotationDeg: c.character.rot };
303
- motion = applySupportRise(motion, supports, { subjectX: anchor.x, subjectY: anchor.y, subjectZ: anchor.z, rotationDeg: anchor.rotationDeg, worldScale: c.character.scale });
304
- const fall = autoRoofDrop(motion, anchor, supports, { worldScale: c.character.scale });
305
- // AutoFall's default shortens clips; preserve the admitted schedule by
306
- // retaining the full landing hold instead of silently dropping beats.
307
- motion = applyAutoFall(motion, fall, { worldScale: c.character.scale, landHoldS: schedule.durationSeconds });
308
- validateClip(motion, schedule.frameCount);
309
- c.motion = { ...motion, personScale: c.character.scale, anchorX: anchor.x, anchorZ: anchor.z, anchorFrame: 0, rotationDeg: anchor.rotationDeg, sceneCalibration: calibration, editSegments: createMotionEdit(motion.frames) };
310
- c.layer = copyLayer(); c.busy = false;
311
- fence(c);
312
- return { ...summary(c), plannedDelta: { characterId: c.character.id, frameCount: motion.frames, fps: 24, scale: c.character.scale, anchor: { x: anchor.x, y: anchor.y, z: anchor.z }, yawDeg: anchor.rotationDeg, clearedIkKeyCount: target.ikState?.keys.size ?? 0 } };
313
- });
314
- admissions.set(request.commandId, { signature, promise });
315
- return promise;
316
- },
317
- verify_motion_candidate(request) {
318
- return operation(request, 'verify', async c => {
319
- if (request.profile !== PROFILE) fail('INVALID_ARGUMENT', 'Unsupported verification profile.');
320
- return evaluate(c);
321
- });
322
- },
323
- repair_motion_candidate(request) {
324
- return operation(request, 'repair', async c => {
325
- if (!c.verification || c.sealed || c.revalidatedEnvironment || !c.verification.repairable) fail('VERIFICATION_FAILED', 'Candidate has no remaining repair permission.');
326
- if (environmentKey(ports.readEnvironment()) !== c.envKey) fail('STALE_ENVIRONMENT', 'Repair cannot restage a changed environment.');
327
- if (!['auto_physics', 'fix_collisions'].includes(request.method)) fail('INVALID_ARGUMENT', 'Unsupported bounded repair method.');
328
- if ((request.protectedFrames ?? []).some(f => !c.protectedFrames.includes(f))) fail('INVALID_ARGUMENT', 'Protection must be admitted during preparation.');
329
- if (request.method === 'auto_physics' ? c.autoAttempted || c.collisionAttempted : c.collisionAttempted || !c.autoAttempted) fail('VERIFICATION_FAILED', 'Repair invocation budget or order exceeded.');
330
- const before = c.verification, draft = copyLayer(c.layer);
331
- c.deadline = now() + (ports.verificationMs ?? 60000);
332
- if (request.method === 'auto_physics') {
333
- c.autoAttempted = true;
334
- if (before.metrics.maxFloorPenetrationM > PHYSICS_LIMITS.floor || before.metrics.maxContactSlipM > PHYSICS_LIMITS.slide || before.metrics.maxContactFloatM > PHYSICS_LIMITS.float || before.metrics.unsupportedFrames > 0) {
335
- c.autoInvocations++;
336
- const result = await (ports.reviewAutoPhysics ?? reviewAutoPhysics)({ rig: c.evaluator.rig, motion: c.motion, chains: c.evaluator.chains, fkJoints: c.evaluator.fkJoints, sourceKeys: draft.keys,
337
- applyRaw: f => poseFrame(c.evaluator, c.motion, null, f), sceneObjects: c.env.objects, floorY: c.env.floor.y, protectedFrames: c.protectedFrames, cache: c.autoCache,
338
- yieldFrame: async () => { checkpoint(c); await yieldTask(); checkpoint(c); } });
339
- draft.keys = copyPhysicsKeys(result.candidate.keys); draft.tracked = new Set(result.candidate.tracked);
340
- }
341
- } else {
342
- c.collisionAttempted = true;
343
- if (before.metrics.supportedCollisionFrames) {
344
- c.collisionInvocations++;
345
- (ports.fixCollisionsRange ?? fixCollisionsRange)({ rig: c.evaluator.rig, chains: c.evaluator.chains, fkJoints: c.evaluator.fkJoints, ikState: draft, startFrame: 0, endFrame: c.motion.frames - 1, floorY: c.env.floor.y,
346
- applyFrame: f => { checkpoint(c); poseFrame(c.evaluator, c.motion, draft, f); }, blockersAt: f => blockersAt(c, f), blendWindow: BLEND });
347
- }
348
- }
349
- checkpoint(c); c.layer = draft; c.revision++;
350
- // All solver writes and their blend ramps finish BEFORE this read-only
351
- // full pass. A regression is a rejected private candidate, never a
352
- // partial change to the visible take or its authored preimage.
353
- const after = await evaluate(c), a = after.metrics, b = before.metrics;
354
- if (a.continuityRegressed || a.protectedPoseError > 1e-8 || a.maxFloorPenetrationM > Math.max(PHYSICS_LIMITS.floor, b.maxFloorPenetrationM) + 1e-8 || a.maxContactSlipM > Math.max(PHYSICS_LIMITS.slide, b.maxContactSlipM) + 1e-8 || a.maxContactFloatM > Math.max(PHYSICS_LIMITS.float, b.maxContactFloatM) + 1e-8 || a.unsupportedFrames > b.unsupportedFrames || a.supportedCollisionFrames > b.supportedCollisionFrames) fail('REPAIR_REGRESSED', 'Final evaluated repair regressed contact, protection, continuity or collisions.');
355
- return { ...summary(c), before, after };
356
- });
357
- },
358
- // Synchronous: no await between the last identity/cancel/physics/gesture
359
- // checks and task 5's publication. No private rig is ever installed.
360
- commit_motion_candidate(request) {
361
- const known = ports.journal.reconcile({ commandId: request.commandId, host: request.binding.host });
362
- if (known.status === 'applied') return known.receipt;
363
- if (known.status === 'not_applied') return { status: 'not_applied', evidence: known.receipt };
364
- const c = candidates.get(request.candidateId);
365
- let committing = false;
366
- try {
367
- if (!c || c.request.commandId !== request.commandId || !equal(c.request.binding, request.binding)) fail('STALE_TARGET', 'Candidate owner changed.');
368
- fence(c);
369
- if (c.busy) fail('TARGET_BUSY', 'Candidate evaluation is still running.');
370
- if (!c.verification || request.candidateRevision !== c.revision || request.verificationId !== c.verification.verificationId || request.expectedTargetToken !== c.request.binding.targetToken || request.jobId !== c.request.jobId || request.artifactId !== c.request.artifactId) fail('STALE_TARGET', 'Commit and verification identities disagree.');
371
- if (environmentKey(ports.readEnvironment()) !== c.envKey || request.expectedPhysicsRevision !== c.verification.physicsRevision) fail('STALE_ENVIRONMENT', 'Commit physics evidence is stale.');
372
- if (ports.readTarget(c.request.binding).busy) fail('TARGET_BUSY', 'A target gesture is in progress.');
373
- if (physicsKeyStamp(c.layer.keys) !== c.verifiedStamp) fail('VERIFICATION_FAILED', 'Keys changed after verification.');
374
- if (c.verification.status !== 'verified' && request.explicitUnverifiedAcceptance !== true) fail('VERIFICATION_FAILED', 'Unverified installation requires explicit acceptance.');
375
- c.sealed = true;
376
- if (!ports.commit) fail('CAPABILITY_MISSING', 'The editor atomic motion owner is not bound.');
377
- const payload = { ...request, motion: c.motion, sourceMotion: c.motion, schedule: c.schedule, ikState: c.layer, scale: c.character.scale, calibration: c.calibration,
378
- verification: receiptVerification(c), repairs: repairCounts(c) };
379
- committing = true;
380
- const receipt = ports.commit(payload);
381
- if (receipt?.then) fail('UNCERTAIN_APPLY', 'Motion publication must be synchronous.');
382
- const validated = validateReceipt(receipt);
383
- if (!validated.ok || validated.status !== 'installed' || validated.commandId !== request.commandId || validated.jobId !== request.jobId || validated.artifactId !== request.artifactId || validated.installed.characterId !== c.character.id || !equal(validated.host, request.binding.host) || !equal(validated.verification, payload.verification)) fail('UNCERTAIN_APPLY', 'Editor returned an uncorrelated installation receipt.');
384
- // The editor owner normally records before acknowledging. Preserve that
385
- // receipt if it did; otherwise record this validated synchronous result.
386
- if (ports.journal.reconcile({ commandId: request.commandId, host: request.binding.host }).status !== 'applied') ports.journal.record(validated);
387
- release(c); return validated;
388
- } catch (error) {
389
- const reconciled = ports.journal.reconcile({ commandId: request.commandId, host: request.binding.host });
390
- if (reconciled.status === 'applied') { if (c) release(c); return reconciled.receipt; }
391
- if (committing) {
392
- // Missing acknowledgement is not non-application proof. Keep ownership
393
- // until the App journals/reconciles, or explicitly disposes this owner.
394
- return { ...rejection(request, error, 'commit'), code: 'UNCERTAIN_APPLY', mutated: 'unknown', preserved: { authoredState: 'unknown' }, recovery: { action: 'reconcile', retryAllowed: false } };
395
- }
396
- if (c && c.request.commandId === request.commandId && equal(c.request.binding, request.binding) && error.code !== 'TARGET_BUSY') release(c);
397
- const receipt = rejection(request, error, 'commit'); ports.journal.record(receipt); return receipt;
398
- }
399
- },
400
- discard_motion_candidate(request) {
401
- const c = candidates.get(request.candidateId);
402
- if (c && (c.request.commandId !== request.commandId || !equal(c.request.binding, request.binding))) return rejection(request, new StudioProtocolError('STALE_TARGET', 'Candidate owner changed.'), 'prepare');
403
- if (c) { c.cancelled = true; if (!c.busy) release(c); else c.controller.abort(); }
404
- return { candidateId: request.candidateId, discarded: true };
405
- },
406
- cancel_motion_install(request) {
407
- const outcome = ports.journal.reconcile({ commandId: request.commandId, host: request.binding.host });
408
- if (outcome.status === 'applied') return { status: 'already_applied', receipt: outcome.receipt };
409
- for (const c of candidates.values()) if (c.request.commandId === request.commandId && equal(c.request.binding, request.binding)) { c.cancelled = true; c.controller.abort(); if (!c.busy) release(c); }
410
- const receipt = rejection(request, new StudioProtocolError('CANCELLED', 'Installation permission revoked.'), 'commit');
411
- // Stop may arrive before admission/prepare has reserved the command.
412
- // Reserve that terminal rejection in the same journal so cancellation is
413
- // structured evidence rather than an INVALID_ARGUMENT from record().
414
- try { ports.journal.begin(request.commandId, JSON.stringify(request)); }
415
- catch (error) {
416
- // A prepare reservation already owns this command signature; Stop is
417
- // still allowed to settle that same command and must not overwrite an
418
- // applied outcome.
419
- if (error?.code !== 'INVALID_ARGUMENT' || ports.journal.reconcile({ commandId: request.commandId, host: request.binding.host }).status !== 'unknown') throw error;
420
- }
421
- ports.journal.record(receipt); return { status: 'not_applied', evidence: receipt };
422
- },
423
- reconcile_studio_command(request) {
424
- const value = ports.journal.reconcile({ commandId: request.commandId, host: request.binding.host });
425
- // Task 2 returns {status,receipt}; task 3 requires positive `evidence`
426
- // for not_applied. Only its recorded rejection supplies that evidence.
427
- return value.status === 'not_applied' ? { ...value, evidence: value.receipt } : value;
428
- },
429
- readEvidence(candidateId) { const c = candidates.get(candidateId); return c?.evidence ? structuredClone({ ...c.evidence, verification: c.verification }) : null; },
430
- cleanup() {
431
- for (const c of candidates.values()) {
432
- try { fence(c); } catch (error) { c.cancelled = true; c.controller.abort(); if (!c.busy) release(c); if (!ports.journal.get(c.request.commandId)) ports.journal.record(rejection(c.request, error, 'prepare')); }
433
- }
434
- for (const [commandId] of admissions) if (![...candidates.values()].some(c => c.request.commandId === commandId)) admissions.delete(commandId);
435
- },
436
- dispose() { disposed = true; for (const c of candidates.values()) { c.cancelled = true; c.controller.abort(); if (!c.busy) release(c); } admissions.clear(); },
437
- get size() { return candidates.size; },
438
- };
439
- return api;
440
189
  }