@hraness/slopcamera 3.2.4

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 (441) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +60 -0
  3. package/PRIVACY.md +46 -0
  4. package/README.md +311 -0
  5. package/SECURITY.md +21 -0
  6. package/apps/desktop/README.md +966 -0
  7. package/apps/desktop/analysis/protocol.ts +223 -0
  8. package/apps/desktop/application/capability-binding.ts +594 -0
  9. package/apps/desktop/application/context.ts +97 -0
  10. package/apps/desktop/application/creative-iteration.ts +1385 -0
  11. package/apps/desktop/application/default-registry.ts +146 -0
  12. package/apps/desktop/application/directing-contract.ts +218 -0
  13. package/apps/desktop/application/directing-plan.ts +178 -0
  14. package/apps/desktop/application/errors.ts +18 -0
  15. package/apps/desktop/application/gateway-port.ts +867 -0
  16. package/apps/desktop/application/html-overlay-browser-runtime.ts +687 -0
  17. package/apps/desktop/application/html-overlay-integrity.ts +337 -0
  18. package/apps/desktop/application/html-overlay-renderer.ts +45 -0
  19. package/apps/desktop/application/index.ts +13 -0
  20. package/apps/desktop/application/operation-completion-checkpoint.ts +333 -0
  21. package/apps/desktop/application/operation-effects.ts +94 -0
  22. package/apps/desktop/application/operation.ts +224 -0
  23. package/apps/desktop/application/operations/analysis/faces.ts +244 -0
  24. package/apps/desktop/application/operations/analysis/music.ts +248 -0
  25. package/apps/desktop/application/operations/analysis/project-auto-zooms.ts +302 -0
  26. package/apps/desktop/application/operations/analysis/project-inactivity.ts +381 -0
  27. package/apps/desktop/application/operations/analysis/scenes.ts +272 -0
  28. package/apps/desktop/application/operations/analysis/shared.ts +202 -0
  29. package/apps/desktop/application/operations/derive/edit-batch.ts +967 -0
  30. package/apps/desktop/application/operations/derive/follow-faces.ts +454 -0
  31. package/apps/desktop/application/operations/edit/bind-candidate-revision.ts +91 -0
  32. package/apps/desktop/application/operations/edit/create-candidate-revision.ts +377 -0
  33. package/apps/desktop/application/operations/edit/create-revision.ts +255 -0
  34. package/apps/desktop/application/operations/edit/freeze-revision.ts +182 -0
  35. package/apps/desktop/application/operations/gateway/image.ts +41 -0
  36. package/apps/desktop/application/operations/gateway/index.ts +17 -0
  37. package/apps/desktop/application/operations/gateway/shared.ts +122 -0
  38. package/apps/desktop/application/operations/gateway/speech.ts +41 -0
  39. package/apps/desktop/application/operations/gateway/transcription.ts +42 -0
  40. package/apps/desktop/application/operations/gateway/video.ts +41 -0
  41. package/apps/desktop/application/operations/index.ts +31 -0
  42. package/apps/desktop/application/operations/iteration/create-candidate.ts +240 -0
  43. package/apps/desktop/application/operations/iteration/create-matrix.ts +117 -0
  44. package/apps/desktop/application/operations/iteration/select.ts +155 -0
  45. package/apps/desktop/application/operations/iteration/shared.ts +250 -0
  46. package/apps/desktop/application/operations/media/audio-effects.ts +376 -0
  47. package/apps/desktop/application/operations/media/binding.ts +79 -0
  48. package/apps/desktop/application/operations/media/capabilities.ts +112 -0
  49. package/apps/desktop/application/operations/media/color-grade.ts +343 -0
  50. package/apps/desktop/application/operations/media/html-overlay.ts +802 -0
  51. package/apps/desktop/application/operations/media/index.ts +8 -0
  52. package/apps/desktop/application/operations/media/ingest.ts +303 -0
  53. package/apps/desktop/application/operations/media/overlay.ts +729 -0
  54. package/apps/desktop/application/operations/media/shared.ts +845 -0
  55. package/apps/desktop/application/operations/media/transform-platform.ts +120 -0
  56. package/apps/desktop/application/operations/project/commit-edits.ts +2054 -0
  57. package/apps/desktop/application/operations/project/promote-selection.ts +495 -0
  58. package/apps/desktop/application/operations/project/snapshot.ts +95 -0
  59. package/apps/desktop/application/operations/recording/index.ts +5 -0
  60. package/apps/desktop/application/operations/recording/pause.ts +34 -0
  61. package/apps/desktop/application/operations/recording/resume.ts +34 -0
  62. package/apps/desktop/application/operations/recording/shared.ts +199 -0
  63. package/apps/desktop/application/operations/recording/start.ts +169 -0
  64. package/apps/desktop/application/operations/recording/stop.ts +34 -0
  65. package/apps/desktop/application/operations/render/bind-candidate-output.ts +324 -0
  66. package/apps/desktop/application/operations/render/materialize-selection.ts +322 -0
  67. package/apps/desktop/application/operations/render/project-caption-plan.ts +287 -0
  68. package/apps/desktop/application/operations/render/project-plan.ts +939 -0
  69. package/apps/desktop/application/operations/render/project-spatial-receipt.ts +70 -0
  70. package/apps/desktop/application/operations/render/project.ts +1922 -0
  71. package/apps/desktop/application/operations/shared.ts +67 -0
  72. package/apps/desktop/application/operations/slopcamera-portable.ts +1188 -0
  73. package/apps/desktop/application/operations/slopcamera-visuals.ts +784 -0
  74. package/apps/desktop/application/operations/spatial-project.ts +209 -0
  75. package/apps/desktop/application/operations/spatial-render.ts +415 -0
  76. package/apps/desktop/application/operations/spatial-scene.ts +97 -0
  77. package/apps/desktop/application/operations/studio.ts +143 -0
  78. package/apps/desktop/application/output-publication-lease.ts +163 -0
  79. package/apps/desktop/application/project-publication-lease.ts +233 -0
  80. package/apps/desktop/application/project-store.ts +196 -0
  81. package/apps/desktop/application/receipts.ts +533 -0
  82. package/apps/desktop/application/registry.ts +182 -0
  83. package/apps/desktop/application/spatial-assets.ts +675 -0
  84. package/apps/desktop/application/spatial-compositor-cadence.ts +23 -0
  85. package/apps/desktop/application/spatial-project-authority.ts +35 -0
  86. package/apps/desktop/application/spatial-project-lease.ts +126 -0
  87. package/apps/desktop/application/spatial-project-render-preparation.ts +225 -0
  88. package/apps/desktop/application/spatial-project-store.ts +286 -0
  89. package/apps/desktop/application/spatial-render-projection.ts +201 -0
  90. package/apps/desktop/application/spatial-render.ts +677 -0
  91. package/apps/desktop/application/spatial-spz.ts +44 -0
  92. package/apps/desktop/application/spatial-world-import.ts +135 -0
  93. package/apps/desktop/application/spatial-world-provenance.ts +46 -0
  94. package/apps/desktop/application/studio-port.ts +58 -0
  95. package/apps/desktop/application/verified-receipt-reconciliation.ts +1574 -0
  96. package/apps/desktop/capture/protocol.ts +689 -0
  97. package/apps/desktop/cli/analyzer.ts +406 -0
  98. package/apps/desktop/cli/args.ts +3181 -0
  99. package/apps/desktop/cli/asset-ingest.ts +747 -0
  100. package/apps/desktop/cli/atomic-render-effects.ts +210 -0
  101. package/apps/desktop/cli/atomic-render-platform.ts +205 -0
  102. package/apps/desktop/cli/atomic-render.ts +50 -0
  103. package/apps/desktop/cli/audio-analysis.ts +252 -0
  104. package/apps/desktop/cli/bounded-map.ts +39 -0
  105. package/apps/desktop/cli/bundle-service.ts +256 -0
  106. package/apps/desktop/cli/capabilities.ts +212 -0
  107. package/apps/desktop/cli/capture-bundle.ts +1117 -0
  108. package/apps/desktop/cli/command-host-resources.ts +251 -0
  109. package/apps/desktop/cli/commands.ts +6836 -0
  110. package/apps/desktop/cli/directing-blob.ts +291 -0
  111. package/apps/desktop/cli/directing-media.ts +517 -0
  112. package/apps/desktop/cli/directing-quote.ts +89 -0
  113. package/apps/desktop/cli/directing-service.ts +288 -0
  114. package/apps/desktop/cli/emoji-assets.ts +538 -0
  115. package/apps/desktop/cli/errors.ts +29 -0
  116. package/apps/desktop/cli/face-analysis-service.ts +940 -0
  117. package/apps/desktop/cli/filter-script.ts +76 -0
  118. package/apps/desktop/cli/gateway-application-port.ts +1142 -0
  119. package/apps/desktop/cli/gateway-credential.ts +81 -0
  120. package/apps/desktop/cli/gateway-media-artifacts.ts +723 -0
  121. package/apps/desktop/cli/gateway-media-catalog.ts +922 -0
  122. package/apps/desktop/cli/gateway-media-service.ts +3106 -0
  123. package/apps/desktop/cli/gateway-media-signature.ts +122 -0
  124. package/apps/desktop/cli/gateway-provider-options.ts +562 -0
  125. package/apps/desktop/cli/gateway-scene-provider.ts +391 -0
  126. package/apps/desktop/cli/help.ts +501 -0
  127. package/apps/desktop/cli/html-overlay-gpu.ts +97 -0
  128. package/apps/desktop/cli/html-overlay-renderer.ts +2478 -0
  129. package/apps/desktop/cli/io.ts +290 -0
  130. package/apps/desktop/cli/main.ts +123 -0
  131. package/apps/desktop/cli/media-effects-service.ts +885 -0
  132. package/apps/desktop/cli/media-ingest-model.ts +364 -0
  133. package/apps/desktop/cli/media-ingest-platform.ts +227 -0
  134. package/apps/desktop/cli/media-ingest-program.ts +200 -0
  135. package/apps/desktop/cli/media-ingest.ts +30 -0
  136. package/apps/desktop/cli/music-analysis-service.ts +434 -0
  137. package/apps/desktop/cli/mutation-lock.ts +530 -0
  138. package/apps/desktop/cli/native-assets.d.ts +12 -0
  139. package/apps/desktop/cli/native-media-runtime.macos.ts +7 -0
  140. package/apps/desktop/cli/overlay-playback.ts +177 -0
  141. package/apps/desktop/cli/paths.ts +265 -0
  142. package/apps/desktop/cli/portable-surface.ts +242 -0
  143. package/apps/desktop/cli/project-camera-receipt.ts +112 -0
  144. package/apps/desktop/cli/project-face-camera.ts +394 -0
  145. package/apps/desktop/cli/project-inactivity-service.ts +552 -0
  146. package/apps/desktop/cli/project-media-integrity.ts +121 -0
  147. package/apps/desktop/cli/project-renderer.ts +1526 -0
  148. package/apps/desktop/cli/project-service.ts +519 -0
  149. package/apps/desktop/cli/project-state-transaction.ts +520 -0
  150. package/apps/desktop/cli/query-limits.ts +1 -0
  151. package/apps/desktop/cli/recording-controller.ts +1567 -0
  152. package/apps/desktop/cli/recording-daemon.ts +931 -0
  153. package/apps/desktop/cli/recording-ref.ts +56 -0
  154. package/apps/desktop/cli/renamed-environment.ts +6 -0
  155. package/apps/desktop/cli/renderer.ts +1439 -0
  156. package/apps/desktop/cli/run-cli-test-helper.ts +60 -0
  157. package/apps/desktop/cli/scene-analysis-service.ts +879 -0
  158. package/apps/desktop/cli/spatial-compositor-verifier.ts +213 -0
  159. package/apps/desktop/cli/spatial-project-service.ts +90 -0
  160. package/apps/desktop/cli/spatial-scene-service.ts +105 -0
  161. package/apps/desktop/cli/spatial-world-service.ts +39 -0
  162. package/apps/desktop/cli/speech-analysis-service.ts +682 -0
  163. package/apps/desktop/cli/studio-assemble.ts +58 -0
  164. package/apps/desktop/cli/studio-command.ts +60 -0
  165. package/apps/desktop/cli/studio-custody.ts +60 -0
  166. package/apps/desktop/cli/studio-encode.ts +216 -0
  167. package/apps/desktop/cli/studio-exr.ts +52 -0
  168. package/apps/desktop/cli/studio-files.ts +179 -0
  169. package/apps/desktop/cli/studio-output-validation.ts +162 -0
  170. package/apps/desktop/cli/studio-process.ts +145 -0
  171. package/apps/desktop/cli/studio-runtime.ts +77 -0
  172. package/apps/desktop/cli/studio-scaffold.ts +70 -0
  173. package/apps/desktop/cli/studio-service.ts +301 -0
  174. package/apps/desktop/cli/studio-spatial-asset.ts +248 -0
  175. package/apps/desktop/cli/studio-template-names.ts +3 -0
  176. package/apps/desktop/cli/studio-workflow.ts +19 -0
  177. package/apps/desktop/cli/time.ts +25 -0
  178. package/apps/desktop/cli/workflow-code.ts +584 -0
  179. package/apps/desktop/cli/workflow-runs.ts +576 -0
  180. package/apps/desktop/code/advanced.ts +139 -0
  181. package/apps/desktop/code/application-node-planner.ts +1288 -0
  182. package/apps/desktop/code/compiler.ts +329 -0
  183. package/apps/desktop/code/contracts.ts +13 -0
  184. package/apps/desktop/code/creative-recipes.ts +94 -0
  185. package/apps/desktop/code/define-workflow.ts +115 -0
  186. package/apps/desktop/code/editing.ts +284 -0
  187. package/apps/desktop/code/file-candidate-provenance.ts +420 -0
  188. package/apps/desktop/code/file-candidate.ts +69 -0
  189. package/apps/desktop/code/graph-builder.ts +10 -0
  190. package/apps/desktop/code/host-resource-policy.ts +55 -0
  191. package/apps/desktop/code/plan-contracts.ts +159 -0
  192. package/apps/desktop/code/planning.ts +361 -0
  193. package/apps/desktop/code/public.ts +134 -0
  194. package/apps/desktop/code/reference-projection.ts +62 -0
  195. package/apps/desktop/code/run-contracts.ts +466 -0
  196. package/apps/desktop/code/run-store.ts +2478 -0
  197. package/apps/desktop/code/runtime-identity.ts +328 -0
  198. package/apps/desktop/code/scheduler.ts +3067 -0
  199. package/apps/desktop/code/semantic-builder.ts +1436 -0
  200. package/apps/desktop/code/source-bundle.ts +1641 -0
  201. package/apps/desktop/code/source-typecheck.ts +211 -0
  202. package/apps/desktop/code/testing.ts +79 -0
  203. package/apps/desktop/code/worker-client.ts +1675 -0
  204. package/apps/desktop/code/worker-effects.ts +141 -0
  205. package/apps/desktop/code/worker-entry.ts +435 -0
  206. package/apps/desktop/code/worker-lease-guardian.ts +179 -0
  207. package/apps/desktop/code/worker-process-identity.ts +484 -0
  208. package/apps/desktop/code/worker-protocol.ts +211 -0
  209. package/apps/desktop/code/workflow-effects.ts +132 -0
  210. package/apps/desktop/contracts/analysis.ts +949 -0
  211. package/apps/desktop/contracts/edit.ts +576 -0
  212. package/apps/desktop/contracts/index.ts +9 -0
  213. package/apps/desktop/contracts/media-effects.ts +139 -0
  214. package/apps/desktop/contracts/project-render.ts +644 -0
  215. package/apps/desktop/contracts/project.ts +740 -0
  216. package/apps/desktop/contracts/recording.ts +1434 -0
  217. package/apps/desktop/contracts/render.ts +334 -0
  218. package/apps/desktop/contracts/runtime.ts +155 -0
  219. package/apps/desktop/contracts/spatial-compositor.ts +58 -0
  220. package/apps/desktop/contracts/spatial-project.ts +159 -0
  221. package/apps/desktop/contracts/spatial-world.ts +48 -0
  222. package/apps/desktop/contracts/video-effects.ts +120 -0
  223. package/apps/desktop/core/alignment-apply.ts +400 -0
  224. package/apps/desktop/core/audio-alignment.ts +466 -0
  225. package/apps/desktop/core/auto-zoom.ts +209 -0
  226. package/apps/desktop/core/canonical-json.ts +14 -0
  227. package/apps/desktop/core/emoji.ts +210 -0
  228. package/apps/desktop/core/events.ts +409 -0
  229. package/apps/desktop/core/face-framing.ts +533 -0
  230. package/apps/desktop/core/face-tracks.ts +326 -0
  231. package/apps/desktop/core/inactivity.ts +137 -0
  232. package/apps/desktop/core/index.ts +30 -0
  233. package/apps/desktop/core/intervals.ts +123 -0
  234. package/apps/desktop/core/montage.ts +236 -0
  235. package/apps/desktop/core/music-analysis.ts +518 -0
  236. package/apps/desktop/core/plan.ts +244 -0
  237. package/apps/desktop/core/project-auto-zoom.ts +75 -0
  238. package/apps/desktop/core/project-camera-spatial.ts +676 -0
  239. package/apps/desktop/core/project-camera.ts +403 -0
  240. package/apps/desktop/core/project-captions.ts +888 -0
  241. package/apps/desktop/core/project-inactivity.ts +125 -0
  242. package/apps/desktop/core/project-metadata.ts +927 -0
  243. package/apps/desktop/core/project-plan.ts +375 -0
  244. package/apps/desktop/core/project-render-plan.ts +303 -0
  245. package/apps/desktop/core/project-time.ts +469 -0
  246. package/apps/desktop/core/render-plan.ts +442 -0
  247. package/apps/desktop/core/scene-cache.ts +174 -0
  248. package/apps/desktop/core/scene-sampling.ts +448 -0
  249. package/apps/desktop/core/spatial-compositor.ts +22 -0
  250. package/apps/desktop/core/spatial-durability.ts +99 -0
  251. package/apps/desktop/core/spatial-project.ts +169 -0
  252. package/apps/desktop/core/spatial-shot-clock.ts +32 -0
  253. package/apps/desktop/core/speech-analysis.ts +333 -0
  254. package/apps/desktop/core/storage.ts +970 -0
  255. package/apps/desktop/core/time-map.ts +91 -0
  256. package/apps/desktop/core/time.ts +61 -0
  257. package/apps/desktop/core/video-effects.ts +660 -0
  258. package/apps/desktop/dist/cli/NebulaSans-Bold-26se8aek.otf +0 -0
  259. package/apps/desktop/dist/cli/NebulaSans-Bold-bcz7y08t.woff2 +0 -0
  260. package/apps/desktop/dist/cli/NebulaSans-Book-5ax05zvn.woff2 +0 -0
  261. package/apps/desktop/dist/cli/NebulaSans-Book-8cenzchw.otf +0 -0
  262. package/apps/desktop/dist/cli/main.js +5086 -0
  263. package/apps/desktop/html-overlay/catalog.ts +171 -0
  264. package/apps/desktop/html-overlay/contracts.ts +372 -0
  265. package/apps/desktop/html-overlay/execution-profile.ts +77 -0
  266. package/apps/desktop/html-overlay/index.ts +98 -0
  267. package/apps/desktop/html-overlay/libraries.ts +263 -0
  268. package/apps/desktop/html-overlay/random.ts +49 -0
  269. package/apps/desktop/html-overlay/runtime.ts +813 -0
  270. package/apps/desktop/html-overlay/scaffolds.ts +792 -0
  271. package/apps/desktop/html-overlay/spark-runtime.ts +88 -0
  272. package/apps/desktop/html-overlay/spatial-splat-runtime.ts +60 -0
  273. package/apps/desktop/html-overlay/spatial.ts +720 -0
  274. package/apps/desktop/studio/assets/README.md +39 -0
  275. package/apps/desktop/studio/assets/contracts.ts +129 -0
  276. package/apps/desktop/studio/assets/network.ts +64 -0
  277. package/apps/desktop/studio/assets/poly-haven.ts +148 -0
  278. package/apps/desktop/studio/drivers/blender_driver.py +466 -0
  279. package/apps/desktop/studio/drivers/cadquery_driver.py +176 -0
  280. package/apps/desktop/studio/education/driver.py +308 -0
  281. package/apps/desktop/studio/education/lesson.py +180 -0
  282. package/apps/desktop/studio/education/toolkit.py +249 -0
  283. package/apps/desktop/workflows/chaptered-demo.ts +48 -0
  284. package/apps/desktop/workflows/creative-iteration.ts +107 -0
  285. package/apps/desktop/workflows/creative-selection.ts +92 -0
  286. package/apps/desktop/workflows/fragments.ts +486 -0
  287. package/apps/desktop/workflows/index.ts +93 -0
  288. package/apps/desktop/workflows/polished-screen-demo.ts +128 -0
  289. package/apps/desktop/workflows/scene-direction.ts +12 -0
  290. package/apps/desktop/workflows/social-variants.ts +75 -0
  291. package/apps/desktop/workflows/talking-head-cleanup.ts +53 -0
  292. package/dist/NebulaSans-Bold-26se8aek.otf +0 -0
  293. package/dist/NebulaSans-Bold-bcz7y08t.woff2 +0 -0
  294. package/dist/NebulaSans-Book-5ax05zvn.woff2 +0 -0
  295. package/dist/NebulaSans-Book-8cenzchw.otf +0 -0
  296. package/dist/cli.js +479 -0
  297. package/dist/code/advanced.js +270 -0
  298. package/dist/code/index.js +2240 -0
  299. package/dist/generate.js +24 -0
  300. package/dist/host-resources.js +26 -0
  301. package/dist/index-42zsesc1.js +2257 -0
  302. package/dist/index-7308egqr.js +48 -0
  303. package/dist/index-8txs6fkn.js +712 -0
  304. package/dist/index-mcy8z0br.js +192 -0
  305. package/dist/index-p63wavx0.js +2029 -0
  306. package/dist/index-r7gdhmsp.js +424 -0
  307. package/dist/index-sh6xbav6.js +1219 -0
  308. package/dist/index-z1w83f81.js +4 -0
  309. package/dist/index-z7239b4h.js +2363 -0
  310. package/dist/index-zfeg9hmw.js +2013 -0
  311. package/dist/index.js +241 -0
  312. package/dist/operations.js +29 -0
  313. package/dist/skill-install-dh0ntqba.js +10 -0
  314. package/dist/vectorize/worker.js +142 -0
  315. package/dist/workflow.js +16 -0
  316. package/docs/README.md +38 -0
  317. package/docs/studio.md +215 -0
  318. package/examples/studio/blender/README.md +26 -0
  319. package/examples/studio/blender/character.py +152 -0
  320. package/examples/studio/blender/cloth.py +101 -0
  321. package/examples/studio/blender/color_chart.py +29 -0
  322. package/examples/studio/blender/fluid.py +78 -0
  323. package/examples/studio/blender/import_model.py +74 -0
  324. package/examples/studio/blender/product.py +53 -0
  325. package/examples/studio/blender/shaded_street.py +481 -0
  326. package/examples/studio/blender/studio_scene.py +147 -0
  327. package/examples/studio/cadquery/README.md +13 -0
  328. package/examples/studio/cadquery/bracket.py +36 -0
  329. package/examples/studio/cadquery/import_step.py +12 -0
  330. package/examples/studio/education/README.md +46 -0
  331. package/examples/studio/education/lesson.json +99 -0
  332. package/examples/studio/education/scene.py +7 -0
  333. package/examples/studio/hybrid-scene.ts +96 -0
  334. package/examples/studio/native-workflow.ts +14 -0
  335. package/examples/studio/vgpu/README.md +64 -0
  336. package/examples/studio/vgpu/heat-field.mjs +290 -0
  337. package/package.json +304 -0
  338. package/packages/scene/src/index.ts +1 -0
  339. package/packages/scene/src/scene.ts +215 -0
  340. package/schema/diagram.schema.json +334 -0
  341. package/skills/slopcamera/SKILL.md +45 -0
  342. package/skills/slopcamera/agents/openai.yaml +4 -0
  343. package/skills/slopcamera/references/customization.md +77 -0
  344. package/skills/slopcamera/references/diagrams.md +153 -0
  345. package/skills/slopcamera/references/directed-scenes.md +42 -0
  346. package/skills/slopcamera/references/directing-video.md +18 -0
  347. package/skills/slopcamera/references/educational-video.md +26 -0
  348. package/skills/slopcamera/references/gateway-media.md +134 -0
  349. package/skills/slopcamera/references/install.md +62 -0
  350. package/skills/slopcamera/references/native-studio.md +31 -0
  351. package/skills/slopcamera/references/reference-led-3d.md +140 -0
  352. package/skills/slopcamera/references/rubber-stamp-examples/poster-example-1.jpg +0 -0
  353. package/skills/slopcamera/references/rubber-stamp-examples/poster-example-2.jpg +0 -0
  354. package/skills/slopcamera/references/rubber-stamp-examples/stamp-style-1.png +0 -0
  355. package/skills/slopcamera/references/rubber-stamp-examples/stamp-style-2.png +0 -0
  356. package/skills/slopcamera/references/rubber-stamp-field-notes.md +144 -0
  357. package/skills/slopcamera/references/vectorization.md +29 -0
  358. package/skills/slopcamera/references/video-projects.md +187 -0
  359. package/skills/slopcamera/references/visual-communication.md +120 -0
  360. package/skills/slopcamera/references/workflows-sdk.md +45 -0
  361. package/skills/slopcamera/scripts/compose-rubber-stamp-field-note.ts +229 -0
  362. package/src/artifacts.ts +106 -0
  363. package/src/assets/fonts/nebula-sans/LICENSE.txt +96 -0
  364. package/src/assets/fonts/nebula-sans/NebulaSans-Bold.otf +0 -0
  365. package/src/assets/fonts/nebula-sans/NebulaSans-Bold.woff2 +0 -0
  366. package/src/assets/fonts/nebula-sans/NebulaSans-Book.otf +0 -0
  367. package/src/assets/fonts/nebula-sans/NebulaSans-Book.woff2 +0 -0
  368. package/src/assets/fonts/nebula-sans/PROVENANCE.md +21 -0
  369. package/src/assets.d.ts +9 -0
  370. package/src/cli.ts +601 -0
  371. package/src/cloud-errors.ts +26 -0
  372. package/src/code/advanced.ts +10 -0
  373. package/src/code/boundary.ts +21 -0
  374. package/src/code/canonical-json.ts +129 -0
  375. package/src/code/compiler.ts +953 -0
  376. package/src/code/contracts.ts +576 -0
  377. package/src/code/define-workflow.ts +334 -0
  378. package/src/code/errors.ts +41 -0
  379. package/src/code/graph-builder.ts +945 -0
  380. package/src/code/index.ts +121 -0
  381. package/src/code/json-snapshot.ts +899 -0
  382. package/src/code/json-utf8.ts +88 -0
  383. package/src/code/portable-builder.ts +136 -0
  384. package/src/code/projection.ts +378 -0
  385. package/src/code/public-operations.ts +450 -0
  386. package/src/code/runtime.ts +580 -0
  387. package/src/code/sha256.ts +36 -0
  388. package/src/config.ts +180 -0
  389. package/src/desktop.ts +253 -0
  390. package/src/fs.ts +10 -0
  391. package/src/generate.ts +704 -0
  392. package/src/host-resource-posix.ts +161 -0
  393. package/src/host-resources.ts +1749 -0
  394. package/src/icons.ts +58 -0
  395. package/src/index.ts +200 -0
  396. package/src/label-layout.ts +130 -0
  397. package/src/layout.ts +413 -0
  398. package/src/lint.ts +127 -0
  399. package/src/mcp/boundary.ts +354 -0
  400. package/src/mcp/index.ts +33 -0
  401. package/src/mcp/server.ts +272 -0
  402. package/src/mcp/tools.ts +963 -0
  403. package/src/mcp/types.ts +71 -0
  404. package/src/operations.ts +775 -0
  405. package/src/parse.ts +861 -0
  406. package/src/process-environment.ts +17 -0
  407. package/src/render.ts +437 -0
  408. package/src/skill-install.ts +49 -0
  409. package/src/spatial-scene/authoring.ts +27 -0
  410. package/src/spatial-scene/camera-track.ts +83 -0
  411. package/src/spatial-scene/contracts.ts +277 -0
  412. package/src/spatial-scene/evaluate.ts +145 -0
  413. package/src/spatial-scene/gltf.ts +558 -0
  414. package/src/spatial-scene/identity.ts +260 -0
  415. package/src/spatial-scene/index.ts +10 -0
  416. package/src/spatial-scene/inspect.ts +68 -0
  417. package/src/spatial-scene/math.ts +280 -0
  418. package/src/spatial-scene/patch.ts +131 -0
  419. package/src/spatial-scene/time.ts +53 -0
  420. package/src/studio/contracts.ts +129 -0
  421. package/src/studio/index.ts +2 -0
  422. package/src/studio/plan.ts +156 -0
  423. package/src/studio/shared.ts +41 -0
  424. package/src/theme.ts +68 -0
  425. package/src/tldr.ts +474 -0
  426. package/src/types.ts +212 -0
  427. package/src/vectorize/archive.ts +101 -0
  428. package/src/vectorize/command.ts +797 -0
  429. package/src/vectorize/index.ts +27 -0
  430. package/src/vectorize/limits.ts +61 -0
  431. package/src/vectorize/metrics.ts +351 -0
  432. package/src/vectorize/pixels.ts +323 -0
  433. package/src/vectorize/supervisor.ts +339 -0
  434. package/src/vectorize/svg.ts +188 -0
  435. package/src/vectorize/tool.ts +542 -0
  436. package/src/vectorize/types.ts +113 -0
  437. package/src/vectorize/vectorize.ts +634 -0
  438. package/src/vectorize/worker-protocol.ts +40 -0
  439. package/src/vectorize/worker.ts +209 -0
  440. package/src/version.ts +1 -0
  441. package/src/workflow.ts +409 -0
@@ -0,0 +1,2054 @@
1
+ import { z } from "zod";
2
+
3
+ import {
4
+ IsoTimestampSchema,
5
+ ProjectCameraMoveSchema,
6
+ ProjectPlacementIdSchema,
7
+ ProjectZoomOperationSchema,
8
+ RecordingIdSchema,
9
+ Sha256Schema,
10
+ ZoomIdSchema,
11
+ type ProjectEditPlanV1,
12
+ type VideoProjectV1,
13
+ } from "../../../contracts";
14
+ import {
15
+ assertProjectCameraMoveBindings,
16
+ assertStaticProjectZoomTargetVisible,
17
+ canonicalSlopcameraPersistenceDocument,
18
+ canonicalJsonSha256,
19
+ cutProjectPlan,
20
+ hashProjectCameraGeometry,
21
+ hashProjectCameraSync,
22
+ hashProjectEditPlan,
23
+ hashPlacementSync,
24
+ hashProjectStructure,
25
+ intersectIntervals,
26
+ normalizeProjectEditPlan,
27
+ projectIntervalHasCompleteSyncCoverage,
28
+ resolveProjectMetadataSourceLayer,
29
+ setProjectSpeed,
30
+ subtractIntervals,
31
+ trimProjectPlan,
32
+ type ResolvedProjectMetadataSourceLayer,
33
+ } from "../../../core";
34
+ import { openRecording } from "../../../cli/bundle-service";
35
+ import { CliError } from "../../../cli/errors";
36
+ import {
37
+ commitProjectStateTransaction,
38
+ } from "../../../cli/project-state-transaction";
39
+ import type { ApplicationContext } from "../../context";
40
+ import { ApplicationError } from "../../errors";
41
+ import type {
42
+ OperationDefinition,
43
+ OperationExecutionContext,
44
+ } from "../../operation";
45
+ import {
46
+ assertProjectEditBasis,
47
+ hashProjectGeneration,
48
+ openProjectSnapshot,
49
+ projectEditBasis,
50
+ ProjectEditBasisSchema,
51
+ type OpenProjectSnapshot,
52
+ type ProjectEditBasis,
53
+ } from "../../project-store";
54
+ import { ProjectEditCommitReceiptSchema } from "../../receipts";
55
+ import {
56
+ ProjectEditBatchV2Schema,
57
+ ProjectEditBatchV3Schema,
58
+ ProjectEditBatchSchema,
59
+ projectEditNormalizationPhaseV3,
60
+ type ManualProjectZoomInputV3,
61
+ type OrderedProjectEditV2,
62
+ type OrderedProjectEditV3,
63
+ type OrderedProjectEdit,
64
+ type ProjectEditNormalizationPhaseV3,
65
+ } from "../derive/edit-batch";
66
+ import {
67
+ ProjectReferenceSchema,
68
+ throwIfAborted,
69
+ } from "../shared";
70
+
71
+ export const CommitProjectEditsInputSchema = z.strictObject({
72
+ batch: ProjectEditBatchSchema,
73
+ basis: ProjectEditBasisSchema,
74
+ project: ProjectReferenceSchema,
75
+ updatedAt: IsoTimestampSchema.optional(),
76
+ });
77
+
78
+ export type CommitProjectEditsInput = z.infer<typeof CommitProjectEditsInputSchema>;
79
+ export type CommitProjectEditsOutput = z.infer<typeof ProjectEditCommitReceiptSchema>;
80
+
81
+ export const ProjectCommitMetadataBindingV2Schema = z.strictObject({
82
+ manifestSha256: Sha256Schema,
83
+ placementId: ProjectPlacementIdSchema,
84
+ recordingId: RecordingIdSchema,
85
+ });
86
+
87
+ export const CommitProjectEditsInputV2Schema = z.strictObject({
88
+ batch: ProjectEditBatchV2Schema,
89
+ basis: ProjectEditBasisSchema,
90
+ metadataBinding: ProjectCommitMetadataBindingV2Schema.optional(),
91
+ project: ProjectReferenceSchema,
92
+ updatedAt: IsoTimestampSchema.optional(),
93
+ }).superRefine((input, context) => {
94
+ const requiresBinding = input.batch.ordered.some(edit => (
95
+ edit.kind === "set-metadata-effects" && metadataEffectsAreEnabled(edit)
96
+ ));
97
+ if (!requiresBinding && input.metadataBinding !== undefined) {
98
+ context.addIssue({
99
+ code: "custom",
100
+ message: "Metadata binding is only valid when this batch enables metadata effects.",
101
+ path: ["metadataBinding"],
102
+ });
103
+ }
104
+ });
105
+
106
+ export type ProjectCommitMetadataBindingV2 = z.infer<
107
+ typeof ProjectCommitMetadataBindingV2Schema
108
+ >;
109
+ export type CommitProjectEditsInputV2 = z.infer<
110
+ typeof CommitProjectEditsInputV2Schema
111
+ >;
112
+
113
+ export const ProjectCommitManualZoomBindingV3Schema = z.strictObject({
114
+ displayId: z.string().min(1).max(256),
115
+ manifestSha256: Sha256Schema,
116
+ placementId: ProjectPlacementIdSchema,
117
+ recordingId: RecordingIdSchema,
118
+ syncSha256: Sha256Schema,
119
+ zoomId: ZoomIdSchema,
120
+ });
121
+
122
+ export const CommitProjectEditsInputV3Schema = z.strictObject({
123
+ batch: ProjectEditBatchV3Schema,
124
+ basis: ProjectEditBasisSchema,
125
+ manualZoomBindings: z.array(
126
+ ProjectCommitManualZoomBindingV3Schema,
127
+ ).max(10_000).optional(),
128
+ metadataBinding: ProjectCommitMetadataBindingV2Schema.optional(),
129
+ project: ProjectReferenceSchema,
130
+ updatedAt: IsoTimestampSchema.optional(),
131
+ }).superRefine((input, context) => {
132
+ const requiresMetadataBinding = input.batch.ordered.some(edit => (
133
+ edit.kind === "set-metadata-effects" && metadataEffectsAreEnabled(edit)
134
+ ));
135
+ if (!requiresMetadataBinding && input.metadataBinding !== undefined) {
136
+ context.addIssue({
137
+ code: "custom",
138
+ message: "Metadata binding is only valid when this batch enables metadata effects.",
139
+ path: ["metadataBinding"],
140
+ });
141
+ }
142
+ const manualZoomIds = input.batch.ordered.flatMap(edit => (
143
+ edit.kind === "add-manual-zooms"
144
+ ? edit.zooms.map(zoom => zoom.zoomId)
145
+ : []
146
+ ));
147
+ if (manualZoomIds.length === 0 && input.manualZoomBindings !== undefined) {
148
+ context.addIssue({
149
+ code: "custom",
150
+ message: "Manual zoom bindings are only valid for a batch that adds manual zooms.",
151
+ path: ["manualZoomBindings"],
152
+ });
153
+ }
154
+ if (
155
+ input.manualZoomBindings !== undefined
156
+ && new Set(input.manualZoomBindings.map(binding => binding.zoomId)).size
157
+ !== input.manualZoomBindings.length
158
+ ) {
159
+ context.addIssue({
160
+ code: "custom",
161
+ message: "Manual zoom bindings must have unique zoom IDs.",
162
+ path: ["manualZoomBindings"],
163
+ });
164
+ }
165
+ });
166
+
167
+ export type ProjectCommitManualZoomBindingV3 = z.infer<
168
+ typeof ProjectCommitManualZoomBindingV3Schema
169
+ >;
170
+ export type CommitProjectEditsInputV3 = z.infer<
171
+ typeof CommitProjectEditsInputV3Schema
172
+ >;
173
+
174
+ export function applyOrderedProjectEdit(
175
+ project: Awaited<ReturnType<typeof openProjectSnapshot>>["project"],
176
+ plan: Awaited<ReturnType<typeof openProjectSnapshot>>["plan"],
177
+ edit: OrderedProjectEdit,
178
+ updatedAt: string,
179
+ normalize = true,
180
+ ) {
181
+ switch (edit.kind) {
182
+ case "cut":
183
+ return normalize
184
+ ? cutProjectPlan(project, plan, edit.range, updatedAt)
185
+ : {
186
+ ...plan,
187
+ keep: subtractIntervals(plan.keep, [edit.range]),
188
+ updatedAt,
189
+ };
190
+ case "trim":
191
+ return normalize
192
+ ? trimProjectPlan(project, plan, edit.range, updatedAt)
193
+ : {
194
+ ...plan,
195
+ keep: intersectIntervals(plan.keep, [edit.range]),
196
+ updatedAt,
197
+ };
198
+ case "speed":
199
+ return normalize
200
+ ? setProjectSpeed(project, plan, edit.range, edit.rate, updatedAt)
201
+ : {
202
+ ...plan,
203
+ speed: [
204
+ ...plan.speed,
205
+ { range: edit.range, rate: edit.rate },
206
+ ],
207
+ updatedAt,
208
+ };
209
+ case "add-zooms":
210
+ return finalizeProjectEditDraft({
211
+ ...plan,
212
+ updatedAt,
213
+ zooms: [...plan.zooms, ...edit.zooms],
214
+ }, normalize);
215
+ case "add-overlays": {
216
+ const existing = new Set(plan.overlays.map(overlay => overlay.overlayId));
217
+ const conflicts = edit.overlays
218
+ .map(overlay => overlay.overlayId)
219
+ .filter(overlayId => existing.has(overlayId))
220
+ .sort();
221
+ if (conflicts.length > 0) {
222
+ throw new ApplicationError(
223
+ "conflict",
224
+ `Overlay ID${conflicts.length === 1 ? "" : "s"} already exist${conflicts.length === 1 ? "s" : ""}: ${conflicts.join(", ")}`,
225
+ );
226
+ }
227
+ return finalizeProjectEditDraft({
228
+ ...plan,
229
+ overlays: [...plan.overlays, ...edit.overlays],
230
+ updatedAt,
231
+ }, normalize);
232
+ }
233
+ case "remove-overlays": {
234
+ const requested = new Set(edit.overlayIds);
235
+ const overlays = plan.overlays.filter(
236
+ overlay => !requested.has(overlay.overlayId),
237
+ );
238
+ if (overlays.length !== plan.overlays.length - requested.size) {
239
+ const existing = new Set(plan.overlays.map(overlay => overlay.overlayId));
240
+ const missing = [...requested].filter(id => !existing.has(id)).sort();
241
+ throw new ApplicationError(
242
+ "not-found",
243
+ `Unknown overlay${missing.length === 1 ? "" : "s"}: ${missing.join(", ")}`,
244
+ );
245
+ }
246
+ return finalizeProjectEditDraft({
247
+ ...plan,
248
+ overlays,
249
+ updatedAt,
250
+ }, normalize);
251
+ }
252
+ }
253
+ }
254
+
255
+ function finalizeProjectEditDraft(
256
+ plan: ProjectEditPlanV1,
257
+ normalize: boolean,
258
+ ): ProjectEditPlanV1 {
259
+ return normalize ? normalizeProjectEditPlan(plan) : plan;
260
+ }
261
+
262
+ export function projectEditCommitReceipt(
263
+ input: {
264
+ readonly batch: {
265
+ readonly ordered: readonly { readonly kind: string }[];
266
+ };
267
+ },
268
+ project: Awaited<ReturnType<typeof openProjectSnapshot>>["project"],
269
+ plan: Awaited<ReturnType<typeof openProjectSnapshot>>["plan"],
270
+ ): CommitProjectEditsOutput {
271
+ const onlyEdit = input.batch.ordered.length === 1
272
+ ? input.batch.ordered[0]
273
+ : undefined;
274
+ return ProjectEditCommitReceiptSchema.parse({
275
+ editBasis: projectEditBasis(project, plan),
276
+ generation: hashProjectGeneration(project, plan),
277
+ operation:
278
+ onlyEdit?.kind === "cut"
279
+ || onlyEdit?.kind === "speed"
280
+ || onlyEdit?.kind === "trim"
281
+ ? onlyEdit.kind
282
+ : "batch",
283
+ planHash: hashProjectEditPlan(plan),
284
+ planId: plan.planId,
285
+ projectId: project.projectId,
286
+ });
287
+ }
288
+
289
+ export function projectEditTransactionId(nodePlanSha256: string): string {
290
+ return `transaction_${nodePlanSha256.slice(0, 32)}`;
291
+ }
292
+
293
+ function metadataEffectsAreEnabled(
294
+ effects: Extract<
295
+ OrderedProjectEditV2,
296
+ { readonly kind: "set-metadata-effects" }
297
+ >,
298
+ ): boolean {
299
+ return effects.clicks.enabled
300
+ || effects.cursor.enabled
301
+ || effects.keystrokes.enabled
302
+ || effects.typedText.enabled;
303
+ }
304
+
305
+ function recordingMetadataLoadError(
306
+ error: unknown,
307
+ placementId: string,
308
+ ): ApplicationError {
309
+ const message = `Recording metadata for placement ${placementId} could not be loaded.`;
310
+ if (!(error instanceof CliError)) {
311
+ return new ApplicationError("invalid-data", message);
312
+ }
313
+ switch (error.code) {
314
+ case "not-found":
315
+ return new ApplicationError("not-found", message);
316
+ case "unavailable":
317
+ return new ApplicationError("unavailable", message);
318
+ case "conflict":
319
+ case "invalid-data":
320
+ case "unsafe-path":
321
+ case "usage":
322
+ return new ApplicationError("invalid-data", message);
323
+ case "ambiguous":
324
+ case "authorization-required":
325
+ case "cancelled":
326
+ case "incompatible":
327
+ case "internal":
328
+ case "subprocess":
329
+ case "unsupported-plan":
330
+ return new ApplicationError("unavailable", message);
331
+ default: {
332
+ const exhaustive: never = error.code;
333
+ return exhaustive;
334
+ }
335
+ }
336
+ }
337
+
338
+ async function assertMetadataEffectsAvailable(
339
+ application: ApplicationContext,
340
+ project: VideoProjectV1,
341
+ edit: Extract<
342
+ OrderedProjectEditV2,
343
+ { readonly kind: "set-metadata-effects" }
344
+ >,
345
+ expectedBinding?: ProjectCommitMetadataBindingV2,
346
+ ): Promise<ProjectCommitMetadataBindingV2 | undefined> {
347
+ if (!metadataEffectsAreEnabled(edit)) return undefined;
348
+ const placementId = edit.metadataPlacementId;
349
+ if (placementId === null) {
350
+ throw new ApplicationError(
351
+ "invalid-data",
352
+ "Enabled metadata effects require a non-null metadata placement.",
353
+ );
354
+ }
355
+ const placement = project.placements.find(
356
+ candidate => candidate.placementId === placementId,
357
+ );
358
+ if (placement === undefined) {
359
+ throw new ApplicationError(
360
+ "not-found",
361
+ `Unknown project metadata placement: ${placementId}`,
362
+ );
363
+ }
364
+ if (!placement.enabled) {
365
+ throw new ApplicationError(
366
+ "conflict",
367
+ `Project metadata placement is disabled: ${placementId}`,
368
+ );
369
+ }
370
+ if (placement.sync.provenance.kind === "unverified") {
371
+ throw new ApplicationError(
372
+ "conflict",
373
+ `Project metadata placement has unverified synchronization: ${placementId}`,
374
+ );
375
+ }
376
+ const asset = project.assets.find(
377
+ candidate => candidate.assetId === placement.assetId,
378
+ );
379
+ if (asset?.source.kind !== "recording") {
380
+ throw new ApplicationError(
381
+ "conflict",
382
+ `Metadata placement ${placementId} is not backed by a Slopcamera recording.`,
383
+ );
384
+ }
385
+ let manifest: Awaited<ReturnType<typeof openRecording>>["manifest"];
386
+ try {
387
+ manifest = (
388
+ await openRecording(
389
+ application.paths.artifactRoot,
390
+ asset.source.recordingId,
391
+ )
392
+ ).manifest;
393
+ } catch (error) {
394
+ throw recordingMetadataLoadError(error, placementId);
395
+ }
396
+ if (manifest.state !== "stopped") {
397
+ throw new ApplicationError(
398
+ "conflict",
399
+ `Metadata effects require an immutable stopped recording for placement ${placementId}.`,
400
+ );
401
+ }
402
+ if (manifest.capture.cursor !== "metadata") {
403
+ throw new ApplicationError(
404
+ "conflict",
405
+ `Interaction metadata capture was disabled for placement ${placementId}.`,
406
+ );
407
+ }
408
+ if (
409
+ (edit.clicks.enabled || edit.keystrokes.enabled || edit.typedText.enabled)
410
+ && manifest.permissions.inputMonitoring !== "authorized"
411
+ ) {
412
+ throw new ApplicationError(
413
+ "conflict",
414
+ `Input monitoring was not authorized for placement ${placementId}.`,
415
+ );
416
+ }
417
+ if (
418
+ edit.typedText.enabled
419
+ && manifest.capture.typedText !== "enabled"
420
+ ) {
421
+ throw new ApplicationError(
422
+ "conflict",
423
+ `Typed-text capture was disabled for placement ${placementId}.`,
424
+ );
425
+ }
426
+ if (
427
+ edit.typedText.enabled
428
+ && manifest.permissions.accessibility !== "authorized"
429
+ ) {
430
+ throw new ApplicationError(
431
+ "conflict",
432
+ `Accessibility metadata was not authorized for placement ${placementId}.`,
433
+ );
434
+ }
435
+ const actual = ProjectCommitMetadataBindingV2Schema.parse({
436
+ manifestSha256: canonicalJsonSha256(manifest),
437
+ placementId,
438
+ recordingId: asset.source.recordingId,
439
+ });
440
+ if (
441
+ expectedBinding !== undefined
442
+ && (
443
+ expectedBinding.manifestSha256 !== actual.manifestSha256
444
+ || expectedBinding.placementId !== actual.placementId
445
+ || expectedBinding.recordingId !== actual.recordingId
446
+ )
447
+ ) {
448
+ throw new ApplicationError(
449
+ "conflict",
450
+ "Recording metadata changed after the project edit batch was prepared.",
451
+ {
452
+ actualManifestSha256: actual.manifestSha256,
453
+ expectedManifestSha256: expectedBinding.manifestSha256,
454
+ placementId,
455
+ },
456
+ );
457
+ }
458
+ return actual;
459
+ }
460
+
461
+ export async function bindProjectCommitEditsInputV2(
462
+ application: ApplicationContext,
463
+ input: unknown,
464
+ ): Promise<CommitProjectEditsInputV2> {
465
+ const parsed = CommitProjectEditsInputV2Schema.parse(input);
466
+ const edit = parsed.batch.ordered.find(
467
+ (
468
+ candidate,
469
+ ): candidate is Extract<
470
+ OrderedProjectEditV2,
471
+ { readonly kind: "set-metadata-effects" }
472
+ > => candidate.kind === "set-metadata-effects",
473
+ );
474
+ if (edit === undefined || !metadataEffectsAreEnabled(edit)) return parsed;
475
+ const snapshot = await openProjectSnapshot(
476
+ application.paths.projectRoot,
477
+ parsed.project,
478
+ );
479
+ return await bindProjectCommitEditsInputV2FromSnapshot(
480
+ application,
481
+ parsed,
482
+ snapshot,
483
+ );
484
+ }
485
+
486
+ /**
487
+ * Host-binding path for callers that acquired a coherent project snapshot
488
+ * before entering this potentially long recording-evidence resolution.
489
+ */
490
+ export async function bindProjectCommitEditsInputV2FromSnapshot(
491
+ application: ApplicationContext,
492
+ input: unknown,
493
+ snapshot: OpenProjectSnapshot,
494
+ ): Promise<CommitProjectEditsInputV2> {
495
+ const parsed = CommitProjectEditsInputV2Schema.parse(input);
496
+ const edit = parsed.batch.ordered.find(
497
+ (
498
+ candidate,
499
+ ): candidate is Extract<
500
+ OrderedProjectEditV2,
501
+ { readonly kind: "set-metadata-effects" }
502
+ > => candidate.kind === "set-metadata-effects",
503
+ );
504
+ if (edit === undefined || !metadataEffectsAreEnabled(edit)) return parsed;
505
+ assertProjectEditBasis(parsed.basis, snapshot);
506
+ const metadataBinding = await assertMetadataEffectsAvailable(
507
+ application,
508
+ snapshot.project,
509
+ edit,
510
+ );
511
+ if (metadataBinding === undefined) {
512
+ throw new ApplicationError(
513
+ "internal",
514
+ "Enabled metadata effects did not produce a recording metadata binding.",
515
+ );
516
+ }
517
+ return CommitProjectEditsInputV2Schema.parse({
518
+ ...parsed,
519
+ metadataBinding,
520
+ });
521
+ }
522
+
523
+ function manualZoomInputs(
524
+ ordered: readonly OrderedProjectEditV3[],
525
+ ): readonly ManualProjectZoomInputV3[] {
526
+ return ordered.flatMap(edit => (
527
+ edit.kind === "add-manual-zooms" ? edit.zooms : []
528
+ ));
529
+ }
530
+
531
+ type ProjectAsset = VideoProjectV1["assets"][number];
532
+ type ProjectPlacement = VideoProjectV1["placements"][number];
533
+ interface ManualZoomRecordingEvidence {
534
+ readonly manifest: Awaited<
535
+ ReturnType<typeof openRecording>
536
+ >["manifest"];
537
+ readonly manifestSha256: string;
538
+ }
539
+
540
+ interface ManualZoomResolutionContext {
541
+ readonly assets: ReadonlyMap<string, ProjectAsset>;
542
+ readonly layers: Map<
543
+ string,
544
+ ResolvedProjectMetadataSourceLayer | null
545
+ >;
546
+ readonly placements: ReadonlyMap<string, ProjectPlacement>;
547
+ readonly recordings: Map<string, Promise<ManualZoomRecordingEvidence>>;
548
+ readonly syncSha256: Map<string, string>;
549
+ readonly onResolutionCacheMiss:
550
+ BindProjectCommitEditsInputV3Options["onResolutionCacheMiss"];
551
+ }
552
+
553
+ export type ManualZoomResolutionCacheKind =
554
+ | "display-layer"
555
+ | "recording"
556
+ | "synchronization";
557
+
558
+ export interface BindProjectCommitEditsInputV3Options {
559
+ /**
560
+ * Test/diagnostic hook. It fires only when host evidence is actually
561
+ * resolved, never for a cache hit.
562
+ */
563
+ readonly onResolutionCacheMiss?: (
564
+ kind: ManualZoomResolutionCacheKind,
565
+ key: string,
566
+ ) => void;
567
+ }
568
+
569
+ function manualZoomResolutionContext(
570
+ project: VideoProjectV1,
571
+ options: BindProjectCommitEditsInputV3Options = {},
572
+ ): ManualZoomResolutionContext {
573
+ return {
574
+ assets: new Map(project.assets.map(asset => [asset.assetId, asset])),
575
+ layers: new Map(),
576
+ placements: new Map(project.placements.map(
577
+ placement => [placement.placementId, placement],
578
+ )),
579
+ recordings: new Map(),
580
+ syncSha256: new Map(),
581
+ onResolutionCacheMiss: options.onResolutionCacheMiss,
582
+ };
583
+ }
584
+
585
+ function indexManualZoomBindings(
586
+ bindings: readonly ProjectCommitManualZoomBindingV3[],
587
+ ): ReadonlyMap<string, ProjectCommitManualZoomBindingV3> {
588
+ const indexed = new Map<string, ProjectCommitManualZoomBindingV3>();
589
+ for (const binding of bindings) {
590
+ if (indexed.has(binding.zoomId)) {
591
+ throw new ApplicationError(
592
+ "incompatible",
593
+ `Manual zoom binding is duplicated: ${binding.zoomId}`,
594
+ );
595
+ }
596
+ indexed.set(binding.zoomId, binding);
597
+ }
598
+ return indexed;
599
+ }
600
+
601
+ function manualZoomBindingFor(
602
+ bindings: ReadonlyMap<string, ProjectCommitManualZoomBindingV3>,
603
+ zoomId: string,
604
+ ): ProjectCommitManualZoomBindingV3 {
605
+ const binding = bindings.get(zoomId);
606
+ if (binding === undefined) {
607
+ throw new ApplicationError(
608
+ "incompatible",
609
+ `Manual zoom ${zoomId} requires a host-owned recording binding.`,
610
+ );
611
+ }
612
+ return binding;
613
+ }
614
+
615
+ async function resolveManualZoomBinding(
616
+ application: ApplicationContext,
617
+ project: VideoProjectV1,
618
+ resolution: ManualZoomResolutionContext,
619
+ input: ManualProjectZoomInputV3,
620
+ defaultPlacementId: VideoProjectV1["referencePlacementId"],
621
+ expectedBinding?: ProjectCommitManualZoomBindingV3,
622
+ ): Promise<ProjectCommitManualZoomBindingV3> {
623
+ const placementId = input.placementId
624
+ ?? expectedBinding?.placementId
625
+ ?? defaultPlacementId;
626
+ const placement = resolution.placements.get(placementId);
627
+ if (placement === undefined) {
628
+ throw new ApplicationError(
629
+ "not-found",
630
+ `Unknown project zoom placement: ${placementId}`,
631
+ );
632
+ }
633
+ if (!placement.enabled) {
634
+ throw new ApplicationError(
635
+ "conflict",
636
+ `Project zoom placement is disabled: ${placementId}`,
637
+ );
638
+ }
639
+ if (placement.sync.provenance.kind === "unverified") {
640
+ throw new ApplicationError(
641
+ "conflict",
642
+ `Project zoom placement has unverified synchronization: ${placementId}`,
643
+ );
644
+ }
645
+ if (input.range.endUs > project.timeline.durationUs) {
646
+ throw new ApplicationError(
647
+ "invalid-data",
648
+ `Manual zoom ${input.zoomId} exceeds the project timeline.`,
649
+ );
650
+ }
651
+ if (!projectIntervalHasCompleteSyncCoverage(
652
+ placement.sync,
653
+ input.range,
654
+ )) {
655
+ throw new ApplicationError(
656
+ "conflict",
657
+ `Manual zoom ${input.zoomId} is not fully covered by placement ${placementId}.`,
658
+ );
659
+ }
660
+ const asset = resolution.assets.get(placement.assetId);
661
+ if (asset?.source.kind !== "recording") {
662
+ throw new ApplicationError(
663
+ "conflict",
664
+ `Zoom placement ${placementId} is not backed by a Slopcamera recording.`,
665
+ );
666
+ }
667
+ let recording: ManualZoomRecordingEvidence;
668
+ try {
669
+ let pending = resolution.recordings.get(asset.source.recordingId);
670
+ if (pending === undefined) {
671
+ resolution.onResolutionCacheMiss?.(
672
+ "recording",
673
+ asset.source.recordingId,
674
+ );
675
+ pending = openRecording(
676
+ application.paths.artifactRoot,
677
+ asset.source.recordingId,
678
+ ).then(opened => ({
679
+ manifest: opened.manifest,
680
+ manifestSha256: canonicalJsonSha256(opened.manifest),
681
+ }));
682
+ resolution.recordings.set(asset.source.recordingId, pending);
683
+ }
684
+ recording = await pending;
685
+ } catch (error) {
686
+ throw recordingMetadataLoadError(error, placementId);
687
+ }
688
+ const { manifest } = recording;
689
+ if (manifest.state !== "stopped") {
690
+ throw new ApplicationError(
691
+ "conflict",
692
+ `Manual zooms require an immutable stopped recording for placement ${placementId}.`,
693
+ );
694
+ }
695
+ const displayId = input.displayId
696
+ ?? expectedBinding?.displayId
697
+ ?? manifest.sources.displays.find(display => display.isPrimary)?.displayId;
698
+ if (displayId === undefined) {
699
+ throw new ApplicationError(
700
+ "not-found",
701
+ `Manual zoom ${input.zoomId} requires a selected or primary display.`,
702
+ );
703
+ }
704
+ const layerKey = `${placementId}\0${displayId}`;
705
+ let layer = resolution.layers.get(layerKey);
706
+ if (layer === undefined) {
707
+ resolution.onResolutionCacheMiss?.("display-layer", layerKey);
708
+ try {
709
+ layer = resolveProjectMetadataSourceLayer(
710
+ manifest,
711
+ asset,
712
+ placement,
713
+ displayId,
714
+ );
715
+ } catch (error) {
716
+ if (!(error instanceof TypeError)) throw error;
717
+ throw new ApplicationError("conflict", error.message);
718
+ }
719
+ resolution.layers.set(layerKey, layer);
720
+ }
721
+ if (layer === null) {
722
+ throw new ApplicationError(
723
+ "conflict",
724
+ `Display ${displayId} is not an enabled video layer on placement ${placementId}.`,
725
+ );
726
+ }
727
+ if (input.target.kind === "point" || input.target.kind === "rect") {
728
+ try {
729
+ assertStaticProjectZoomTargetVisible(input.target, {
730
+ display: layer.display,
731
+ presentation: layer.presentation,
732
+ stream: layer.stream,
733
+ });
734
+ } catch (error) {
735
+ if (!(error instanceof TypeError)) throw error;
736
+ throw new ApplicationError(
737
+ "invalid-data",
738
+ `Manual zoom ${input.zoomId} has a target outside the visible display layer: ${error.message}`,
739
+ );
740
+ }
741
+ }
742
+ if (
743
+ input.target.kind === "cursor"
744
+ && manifest.capture.cursor !== "metadata"
745
+ ) {
746
+ throw new ApplicationError(
747
+ "conflict",
748
+ `Cursor metadata capture was disabled for placement ${placementId}.`,
749
+ );
750
+ }
751
+ if (
752
+ input.target.kind === "window"
753
+ && (
754
+ manifest.capture.windowMetadata === "disabled"
755
+ || manifest.permissions.windowMetadata !== "authorized"
756
+ )
757
+ ) {
758
+ throw new ApplicationError(
759
+ "conflict",
760
+ `Window metadata was unavailable for placement ${placementId}.`,
761
+ );
762
+ }
763
+ if (
764
+ input.target.kind === "focused-input"
765
+ && manifest.permissions.accessibility !== "authorized"
766
+ ) {
767
+ throw new ApplicationError(
768
+ "conflict",
769
+ `Accessibility metadata was unavailable for placement ${placementId}.`,
770
+ );
771
+ }
772
+ let syncSha256 = resolution.syncSha256.get(placementId);
773
+ if (syncSha256 === undefined) {
774
+ resolution.onResolutionCacheMiss?.("synchronization", placementId);
775
+ syncSha256 = hashPlacementSync(placement);
776
+ resolution.syncSha256.set(placementId, syncSha256);
777
+ }
778
+ const actual = ProjectCommitManualZoomBindingV3Schema.parse({
779
+ displayId,
780
+ manifestSha256: recording.manifestSha256,
781
+ placementId,
782
+ recordingId: asset.source.recordingId,
783
+ syncSha256,
784
+ zoomId: input.zoomId,
785
+ });
786
+ if (
787
+ expectedBinding !== undefined
788
+ && canonicalJsonSha256(actual) !== canonicalJsonSha256(expectedBinding)
789
+ ) {
790
+ throw new ApplicationError(
791
+ "conflict",
792
+ `Manual zoom ${input.zoomId} bindings changed after the edit batch was prepared.`,
793
+ {
794
+ actualManifestSha256: actual.manifestSha256,
795
+ expectedManifestSha256: expectedBinding.manifestSha256,
796
+ placementId,
797
+ },
798
+ );
799
+ }
800
+ return actual;
801
+ }
802
+
803
+ export async function bindProjectCommitEditsInputV3(
804
+ application: ApplicationContext,
805
+ input: unknown,
806
+ options: BindProjectCommitEditsInputV3Options = {},
807
+ ): Promise<CommitProjectEditsInputV3> {
808
+ const parsed = CommitProjectEditsInputV3Schema.parse(input);
809
+ const metadataEdit = parsed.batch.ordered.find(
810
+ (
811
+ candidate,
812
+ ): candidate is Extract<
813
+ OrderedProjectEditV3,
814
+ { readonly kind: "set-metadata-effects" }
815
+ > => candidate.kind === "set-metadata-effects",
816
+ );
817
+ const zooms = manualZoomInputs(parsed.batch.ordered);
818
+ const requiresMetadataBinding = metadataEdit !== undefined
819
+ && metadataEffectsAreEnabled(metadataEdit);
820
+ if (!requiresMetadataBinding && zooms.length === 0) return parsed;
821
+ const snapshot = await openProjectSnapshot(
822
+ application.paths.projectRoot,
823
+ parsed.project,
824
+ );
825
+ return await bindProjectCommitEditsInputV3FromSnapshot(
826
+ application,
827
+ parsed,
828
+ snapshot,
829
+ options,
830
+ );
831
+ }
832
+
833
+ /**
834
+ * Host-binding path for callers that acquired a coherent project snapshot
835
+ * before resolving recording and display evidence.
836
+ */
837
+ export async function bindProjectCommitEditsInputV3FromSnapshot(
838
+ application: ApplicationContext,
839
+ input: unknown,
840
+ snapshot: OpenProjectSnapshot,
841
+ options: BindProjectCommitEditsInputV3Options = {},
842
+ ): Promise<CommitProjectEditsInputV3> {
843
+ const parsed = CommitProjectEditsInputV3Schema.parse(input);
844
+ const metadataEdit = parsed.batch.ordered.find(
845
+ (
846
+ candidate,
847
+ ): candidate is Extract<
848
+ OrderedProjectEditV3,
849
+ { readonly kind: "set-metadata-effects" }
850
+ > => candidate.kind === "set-metadata-effects",
851
+ );
852
+ const zooms = manualZoomInputs(parsed.batch.ordered);
853
+ const requiresMetadataBinding = metadataEdit !== undefined
854
+ && metadataEffectsAreEnabled(metadataEdit);
855
+ if (!requiresMetadataBinding && zooms.length === 0) return parsed;
856
+ assertProjectEditBasis(parsed.basis, snapshot);
857
+ const metadataBinding = requiresMetadataBinding
858
+ ? await assertMetadataEffectsAvailable(
859
+ application,
860
+ snapshot.project,
861
+ metadataEdit,
862
+ )
863
+ : undefined;
864
+ const defaultPlacementId = snapshot.plan.effects.metadataPlacementId
865
+ ?? snapshot.project.referencePlacementId;
866
+ const resolution = manualZoomResolutionContext(
867
+ snapshot.project,
868
+ options,
869
+ );
870
+ const manualZoomBindings: ProjectCommitManualZoomBindingV3[] = [];
871
+ for (const zoom of zooms) {
872
+ manualZoomBindings.push(await resolveManualZoomBinding(
873
+ application,
874
+ snapshot.project,
875
+ resolution,
876
+ zoom,
877
+ defaultPlacementId,
878
+ ));
879
+ }
880
+ return CommitProjectEditsInputV3Schema.parse({
881
+ ...parsed,
882
+ ...(manualZoomBindings.length === 0 ? {} : { manualZoomBindings }),
883
+ ...(metadataBinding === undefined ? {} : { metadataBinding }),
884
+ });
885
+ }
886
+
887
+ function manualCameraMove(
888
+ project: VideoProjectV1,
889
+ input: Extract<
890
+ OrderedProjectEditV2,
891
+ { readonly kind: "add-manual-camera-moves" }
892
+ >["cameraMoves"][number],
893
+ ) {
894
+ const placement = project.placements.find(
895
+ candidate => candidate.placementId === input.placementId,
896
+ );
897
+ if (placement === undefined) {
898
+ throw new ApplicationError(
899
+ "not-found",
900
+ `Unknown camera placement: ${input.placementId}`,
901
+ );
902
+ }
903
+ const asset = project.assets.find(
904
+ candidate => candidate.assetId === placement.assetId,
905
+ );
906
+ const stream = asset?.streams.find(
907
+ candidate => candidate.streamId === input.streamId,
908
+ );
909
+ if (stream?.kind !== "video") {
910
+ throw new ApplicationError(
911
+ "not-found",
912
+ `Unknown camera video stream: ${input.placementId}:${input.streamId}`,
913
+ );
914
+ }
915
+ const configured = placement.video.find(
916
+ candidate => candidate.streamId === input.streamId,
917
+ );
918
+ if (configured === undefined) {
919
+ throw new ApplicationError(
920
+ "conflict",
921
+ `Camera video stream is not configured on placement ${input.placementId}.`,
922
+ );
923
+ }
924
+ if (!placement.enabled || !configured.presentation.enabled) {
925
+ throw new ApplicationError(
926
+ "conflict",
927
+ `Camera move ${input.cameraMoveId} targets a disabled video layer.`,
928
+ );
929
+ }
930
+ try {
931
+ const move = ProjectCameraMoveSchema.parse({
932
+ ...input,
933
+ binding: {
934
+ geometrySha256: hashProjectCameraGeometry(
935
+ project,
936
+ input.placementId,
937
+ input.streamId,
938
+ ),
939
+ syncSha256: hashProjectCameraSync(placement),
940
+ },
941
+ origin: { kind: "manual" },
942
+ });
943
+ assertProjectCameraMoveBindings(project, move);
944
+ return move;
945
+ } catch (error) {
946
+ if (error instanceof ApplicationError) throw error;
947
+ throw new ApplicationError(
948
+ "invalid-data",
949
+ `Manual camera move ${input.cameraMoveId} is invalid for the current project.`,
950
+ );
951
+ }
952
+ }
953
+
954
+ async function applyOrderedProjectEditV2(
955
+ application: ApplicationContext,
956
+ project: VideoProjectV1,
957
+ plan: ProjectEditPlanV1,
958
+ edit: OrderedProjectEditV2,
959
+ metadataBinding: ProjectCommitMetadataBindingV2 | undefined,
960
+ updatedAt: string,
961
+ normalize = true,
962
+ ): Promise<ProjectEditPlanV1> {
963
+ switch (edit.kind) {
964
+ case "cut":
965
+ case "trim":
966
+ case "speed":
967
+ case "add-overlays":
968
+ case "remove-overlays":
969
+ return applyOrderedProjectEdit(
970
+ project,
971
+ plan,
972
+ edit,
973
+ updatedAt,
974
+ normalize,
975
+ );
976
+ case "add-zooms": {
977
+ const existing = new Set(
978
+ plan.zooms.map(zoom => zoom.operation.zoomId),
979
+ );
980
+ const conflicts = edit.zooms
981
+ .map(zoom => zoom.operation.zoomId)
982
+ .filter(zoomId => existing.has(zoomId))
983
+ .sort();
984
+ if (conflicts.length > 0) {
985
+ throw new ApplicationError(
986
+ "conflict",
987
+ `Zoom ID${conflicts.length === 1 ? "" : "s"} already exist${conflicts.length === 1 ? "s" : ""}: ${conflicts.join(", ")}`,
988
+ );
989
+ }
990
+ try {
991
+ return finalizeProjectEditDraft({
992
+ ...plan,
993
+ updatedAt,
994
+ zooms: [...plan.zooms, ...edit.zooms],
995
+ }, normalize);
996
+ } catch (error) {
997
+ if (error instanceof ApplicationError) throw error;
998
+ throw new ApplicationError(
999
+ "conflict",
1000
+ "Zooms are incompatible with the current project edit plan.",
1001
+ );
1002
+ }
1003
+ }
1004
+ case "remove-zooms": {
1005
+ const requested = new Set(edit.zoomIds);
1006
+ const existing = new Set(
1007
+ plan.zooms.map(zoom => zoom.operation.zoomId),
1008
+ );
1009
+ const missing = [...requested]
1010
+ .filter(zoomId => !existing.has(zoomId))
1011
+ .sort();
1012
+ if (missing.length > 0) {
1013
+ throw new ApplicationError(
1014
+ "not-found",
1015
+ `Unknown zoom${missing.length === 1 ? "" : "s"}: ${missing.join(", ")}`,
1016
+ );
1017
+ }
1018
+ return finalizeProjectEditDraft({
1019
+ ...plan,
1020
+ updatedAt,
1021
+ zooms: plan.zooms.filter(
1022
+ zoom => !requested.has(zoom.operation.zoomId),
1023
+ ),
1024
+ }, normalize);
1025
+ }
1026
+ case "set-metadata-effects":
1027
+ await assertMetadataEffectsAvailable(
1028
+ application,
1029
+ project,
1030
+ edit,
1031
+ metadataBinding,
1032
+ );
1033
+ return finalizeProjectEditDraft({
1034
+ ...plan,
1035
+ effects: {
1036
+ clicks: edit.clicks,
1037
+ cursor: edit.cursor,
1038
+ keystrokes: edit.keystrokes,
1039
+ metadataPlacementId: edit.metadataPlacementId,
1040
+ typedText: edit.typedText,
1041
+ },
1042
+ updatedAt,
1043
+ }, normalize);
1044
+ case "add-manual-camera-moves": {
1045
+ const existing = new Set(
1046
+ plan.cameraMoves.map(move => move.cameraMoveId),
1047
+ );
1048
+ const conflicts = edit.cameraMoves
1049
+ .map(move => move.cameraMoveId)
1050
+ .filter(cameraMoveId => existing.has(cameraMoveId))
1051
+ .sort();
1052
+ if (conflicts.length > 0) {
1053
+ throw new ApplicationError(
1054
+ "conflict",
1055
+ `Camera move ID${conflicts.length === 1 ? "" : "s"} already exist${conflicts.length === 1 ? "s" : ""}: ${conflicts.join(", ")}`,
1056
+ );
1057
+ }
1058
+ const additions = edit.cameraMoves.map(
1059
+ move => manualCameraMove(project, move),
1060
+ );
1061
+ try {
1062
+ return finalizeProjectEditDraft({
1063
+ ...plan,
1064
+ cameraMoves: [...plan.cameraMoves, ...additions],
1065
+ updatedAt,
1066
+ }, normalize);
1067
+ } catch (error) {
1068
+ if (error instanceof ApplicationError) throw error;
1069
+ throw new ApplicationError(
1070
+ "conflict",
1071
+ "Manual camera moves are incompatible with the current project edit plan.",
1072
+ );
1073
+ }
1074
+ }
1075
+ case "remove-camera-moves": {
1076
+ const requested = new Set(edit.cameraMoveIds);
1077
+ const existing = new Set(
1078
+ plan.cameraMoves.map(move => move.cameraMoveId),
1079
+ );
1080
+ const missing = [...requested]
1081
+ .filter(cameraMoveId => !existing.has(cameraMoveId))
1082
+ .sort();
1083
+ if (missing.length > 0) {
1084
+ throw new ApplicationError(
1085
+ "not-found",
1086
+ `Unknown camera move${missing.length === 1 ? "" : "s"}: ${missing.join(", ")}`,
1087
+ );
1088
+ }
1089
+ return finalizeProjectEditDraft({
1090
+ ...plan,
1091
+ cameraMoves: plan.cameraMoves.filter(
1092
+ move => !requested.has(move.cameraMoveId),
1093
+ ),
1094
+ updatedAt,
1095
+ }, normalize);
1096
+ }
1097
+ default: {
1098
+ const exhaustive: never = edit;
1099
+ return exhaustive;
1100
+ }
1101
+ }
1102
+ }
1103
+
1104
+ function manualProjectZoom(
1105
+ input: ManualProjectZoomInputV3,
1106
+ binding: ProjectCommitManualZoomBindingV3,
1107
+ ) {
1108
+ if (
1109
+ (input.placementId !== undefined && input.placementId !== binding.placementId)
1110
+ || (input.displayId !== undefined && input.displayId !== binding.displayId)
1111
+ || input.zoomId !== binding.zoomId
1112
+ ) {
1113
+ throw new ApplicationError(
1114
+ "conflict",
1115
+ `Manual zoom ${input.zoomId} does not match its host-owned binding.`,
1116
+ );
1117
+ }
1118
+ try {
1119
+ return ProjectZoomOperationSchema.parse({
1120
+ operation: {
1121
+ displayId: binding.displayId,
1122
+ easing: input.easing,
1123
+ enterDurationUs: input.enterDurationUs,
1124
+ exitDurationUs: input.exitDurationUs,
1125
+ kind: "manual",
1126
+ range: input.range,
1127
+ scale: input.scale,
1128
+ target: input.target,
1129
+ zoomId: input.zoomId,
1130
+ },
1131
+ placementId: binding.placementId,
1132
+ });
1133
+ } catch {
1134
+ throw new ApplicationError(
1135
+ "invalid-data",
1136
+ `Manual zoom ${input.zoomId} is invalid for the current project.`,
1137
+ );
1138
+ }
1139
+ }
1140
+
1141
+ function removeKnownProjectEditIds<Id extends string>(
1142
+ available: Set<Id>,
1143
+ ids: readonly Id[],
1144
+ label: string,
1145
+ ): void {
1146
+ const requested = new Set(ids);
1147
+ const missing = [...requested]
1148
+ .filter(id => !available.has(id))
1149
+ .sort();
1150
+ if (missing.length > 0) {
1151
+ throw new ApplicationError(
1152
+ "not-found",
1153
+ `Unknown ${label}${missing.length === 1 ? "" : "s"}: ${missing.join(", ")}`,
1154
+ );
1155
+ }
1156
+ for (const id of requested) available.delete(id);
1157
+ }
1158
+
1159
+ async function applyOrderedProjectEditV3(
1160
+ application: ApplicationContext,
1161
+ project: VideoProjectV1,
1162
+ plan: ProjectEditPlanV1,
1163
+ edit: OrderedProjectEditV3,
1164
+ metadataBinding: ProjectCommitMetadataBindingV2 | undefined,
1165
+ manualZoomBindings: ReadonlyMap<
1166
+ string,
1167
+ ProjectCommitManualZoomBindingV3
1168
+ >,
1169
+ updatedAt: string,
1170
+ normalize = true,
1171
+ ): Promise<ProjectEditPlanV1> {
1172
+ if (edit.kind !== "add-manual-zooms") {
1173
+ return await applyOrderedProjectEditV2(
1174
+ application,
1175
+ project,
1176
+ plan,
1177
+ edit,
1178
+ metadataBinding,
1179
+ updatedAt,
1180
+ normalize,
1181
+ );
1182
+ }
1183
+ const existing = new Set(
1184
+ plan.zooms.map(zoom => zoom.operation.zoomId),
1185
+ );
1186
+ const conflicts = edit.zooms
1187
+ .map(zoom => zoom.zoomId)
1188
+ .filter(zoomId => existing.has(zoomId))
1189
+ .sort();
1190
+ if (conflicts.length > 0) {
1191
+ throw new ApplicationError(
1192
+ "conflict",
1193
+ `Zoom ID${conflicts.length === 1 ? "" : "s"} already exist${conflicts.length === 1 ? "s" : ""}: ${conflicts.join(", ")}`,
1194
+ );
1195
+ }
1196
+ const additions = edit.zooms.map(zoom => manualProjectZoom(
1197
+ zoom,
1198
+ manualZoomBindingFor(manualZoomBindings, zoom.zoomId),
1199
+ ));
1200
+ try {
1201
+ return finalizeProjectEditDraft({
1202
+ ...plan,
1203
+ updatedAt,
1204
+ zooms: [...plan.zooms, ...additions],
1205
+ }, normalize);
1206
+ } catch {
1207
+ throw new ApplicationError(
1208
+ "conflict",
1209
+ "Manual zooms are incompatible with the current project edit plan.",
1210
+ );
1211
+ }
1212
+ }
1213
+
1214
+ async function applyOrderedProjectEditPhaseV3(
1215
+ application: ApplicationContext,
1216
+ project: VideoProjectV1,
1217
+ plan: ProjectEditPlanV1,
1218
+ edits: readonly OrderedProjectEditV3[],
1219
+ metadataBinding: ProjectCommitMetadataBindingV2 | undefined,
1220
+ manualZoomBindings: ReadonlyMap<
1221
+ string,
1222
+ ProjectCommitManualZoomBindingV3
1223
+ >,
1224
+ updatedAt: string,
1225
+ abortSignal: AbortSignal,
1226
+ ): Promise<ProjectEditPlanV1> {
1227
+ const first = edits[0];
1228
+ if (first === undefined) return plan;
1229
+ const phase: ProjectEditNormalizationPhaseV3 =
1230
+ projectEditNormalizationPhaseV3(first);
1231
+ if (edits.some(edit => projectEditNormalizationPhaseV3(edit) !== phase)) {
1232
+ throw new ApplicationError(
1233
+ "internal",
1234
+ "A project edit normalization phase contained incompatible edits.",
1235
+ );
1236
+ }
1237
+
1238
+ switch (phase) {
1239
+ case "structural": {
1240
+ const cuts: Extract<
1241
+ OrderedProjectEditV3,
1242
+ { readonly kind: "cut" }
1243
+ >["range"][] = [];
1244
+ const speed: ProjectEditPlanV1["speed"][number][] = [];
1245
+ let trimStartUs = 0;
1246
+ let trimEndUs = project.timeline.durationUs;
1247
+ let hasTrim = false;
1248
+ for (const edit of edits) {
1249
+ throwIfAborted(abortSignal);
1250
+ switch (edit.kind) {
1251
+ case "cut":
1252
+ cuts.push(edit.range);
1253
+ break;
1254
+ case "speed":
1255
+ speed.push({ range: edit.range, rate: edit.rate });
1256
+ break;
1257
+ case "trim":
1258
+ hasTrim = true;
1259
+ trimStartUs = Math.max(trimStartUs, edit.range.startUs);
1260
+ trimEndUs = Math.min(trimEndUs, edit.range.endUs);
1261
+ break;
1262
+ case "add-manual-camera-moves":
1263
+ case "add-manual-zooms":
1264
+ case "add-overlays":
1265
+ case "add-zooms":
1266
+ case "remove-camera-moves":
1267
+ case "remove-overlays":
1268
+ case "remove-zooms":
1269
+ case "set-metadata-effects":
1270
+ throw new ApplicationError(
1271
+ "internal",
1272
+ "A structural project edit phase contained a non-structural edit.",
1273
+ );
1274
+ }
1275
+ }
1276
+ const trimmedKeep = !hasTrim
1277
+ ? plan.keep
1278
+ : trimStartUs >= trimEndUs
1279
+ ? []
1280
+ : intersectIntervals(plan.keep, [{
1281
+ endUs: trimEndUs,
1282
+ startUs: trimStartUs,
1283
+ }]);
1284
+ return {
1285
+ ...plan,
1286
+ keep: subtractIntervals(trimmedKeep, cuts),
1287
+ speed: [...plan.speed, ...speed],
1288
+ updatedAt,
1289
+ };
1290
+ }
1291
+ case "add": {
1292
+ const cameraMoveIds = new Set(
1293
+ plan.cameraMoves.map(move => move.cameraMoveId),
1294
+ );
1295
+ const overlayIds = new Set(
1296
+ plan.overlays.map(overlay => overlay.overlayId),
1297
+ );
1298
+ const zoomIds = new Set(
1299
+ plan.zooms.map(zoom => zoom.operation.zoomId),
1300
+ );
1301
+ const cameraMoves: ProjectEditPlanV1["cameraMoves"][number][] = [];
1302
+ const overlays: ProjectEditPlanV1["overlays"][number][] = [];
1303
+ const zooms: ProjectEditPlanV1["zooms"][number][] = [];
1304
+ for (const edit of edits) {
1305
+ throwIfAborted(abortSignal);
1306
+ switch (edit.kind) {
1307
+ case "add-manual-camera-moves":
1308
+ for (const input of edit.cameraMoves) {
1309
+ if (cameraMoveIds.has(input.cameraMoveId)) {
1310
+ throw new ApplicationError(
1311
+ "conflict",
1312
+ `Camera move ID already exists: ${input.cameraMoveId}`,
1313
+ );
1314
+ }
1315
+ cameraMoveIds.add(input.cameraMoveId);
1316
+ cameraMoves.push(manualCameraMove(project, input));
1317
+ }
1318
+ break;
1319
+ case "add-manual-zooms":
1320
+ for (const input of edit.zooms) {
1321
+ if (zoomIds.has(input.zoomId)) {
1322
+ throw new ApplicationError(
1323
+ "conflict",
1324
+ `Zoom ID already exists: ${input.zoomId}`,
1325
+ );
1326
+ }
1327
+ zoomIds.add(input.zoomId);
1328
+ zooms.push(manualProjectZoom(
1329
+ input,
1330
+ manualZoomBindingFor(manualZoomBindings, input.zoomId),
1331
+ ));
1332
+ }
1333
+ break;
1334
+ case "add-overlays":
1335
+ for (const overlay of edit.overlays) {
1336
+ if (overlayIds.has(overlay.overlayId)) {
1337
+ throw new ApplicationError(
1338
+ "conflict",
1339
+ `Overlay ID already exists: ${overlay.overlayId}`,
1340
+ );
1341
+ }
1342
+ overlayIds.add(overlay.overlayId);
1343
+ overlays.push(overlay);
1344
+ }
1345
+ break;
1346
+ case "add-zooms":
1347
+ for (const zoom of edit.zooms) {
1348
+ if (zoomIds.has(zoom.operation.zoomId)) {
1349
+ throw new ApplicationError(
1350
+ "conflict",
1351
+ `Zoom ID already exists: ${zoom.operation.zoomId}`,
1352
+ );
1353
+ }
1354
+ zoomIds.add(zoom.operation.zoomId);
1355
+ zooms.push(zoom);
1356
+ }
1357
+ break;
1358
+ case "cut":
1359
+ case "remove-camera-moves":
1360
+ case "remove-overlays":
1361
+ case "remove-zooms":
1362
+ case "set-metadata-effects":
1363
+ case "speed":
1364
+ case "trim":
1365
+ throw new ApplicationError(
1366
+ "internal",
1367
+ "An additive project edit phase contained a non-additive edit.",
1368
+ );
1369
+ }
1370
+ }
1371
+ return {
1372
+ ...plan,
1373
+ cameraMoves: [...plan.cameraMoves, ...cameraMoves],
1374
+ overlays: [...plan.overlays, ...overlays],
1375
+ updatedAt,
1376
+ zooms: [...plan.zooms, ...zooms],
1377
+ };
1378
+ }
1379
+ case "remove": {
1380
+ const cameraMoveIds = new Set(
1381
+ plan.cameraMoves.map(move => move.cameraMoveId),
1382
+ );
1383
+ const overlayIds = new Set(
1384
+ plan.overlays.map(overlay => overlay.overlayId),
1385
+ );
1386
+ const zoomIds = new Set(
1387
+ plan.zooms.map(zoom => zoom.operation.zoomId),
1388
+ );
1389
+ for (const edit of edits) {
1390
+ throwIfAborted(abortSignal);
1391
+ switch (edit.kind) {
1392
+ case "remove-camera-moves":
1393
+ removeKnownProjectEditIds(
1394
+ cameraMoveIds,
1395
+ edit.cameraMoveIds,
1396
+ "camera move",
1397
+ );
1398
+ break;
1399
+ case "remove-overlays":
1400
+ removeKnownProjectEditIds(
1401
+ overlayIds,
1402
+ edit.overlayIds,
1403
+ "overlay",
1404
+ );
1405
+ break;
1406
+ case "remove-zooms":
1407
+ removeKnownProjectEditIds(
1408
+ zoomIds,
1409
+ edit.zoomIds,
1410
+ "zoom",
1411
+ );
1412
+ break;
1413
+ case "add-manual-camera-moves":
1414
+ case "add-manual-zooms":
1415
+ case "add-overlays":
1416
+ case "add-zooms":
1417
+ case "cut":
1418
+ case "set-metadata-effects":
1419
+ case "speed":
1420
+ case "trim":
1421
+ throw new ApplicationError(
1422
+ "internal",
1423
+ "A removal project edit phase contained a non-removal edit.",
1424
+ );
1425
+ }
1426
+ }
1427
+ return {
1428
+ ...plan,
1429
+ cameraMoves: plan.cameraMoves.filter(
1430
+ move => cameraMoveIds.has(move.cameraMoveId),
1431
+ ),
1432
+ overlays: plan.overlays.filter(
1433
+ overlay => overlayIds.has(overlay.overlayId),
1434
+ ),
1435
+ updatedAt,
1436
+ zooms: plan.zooms.filter(
1437
+ zoom => zoomIds.has(zoom.operation.zoomId),
1438
+ ),
1439
+ };
1440
+ }
1441
+ case "metadata": {
1442
+ const edit = first.kind === "set-metadata-effects"
1443
+ ? first
1444
+ : undefined;
1445
+ if (edit === undefined || edits.length !== 1) {
1446
+ throw new ApplicationError(
1447
+ "internal",
1448
+ "A metadata project edit phase must contain exactly one metadata edit.",
1449
+ );
1450
+ }
1451
+ return await applyOrderedProjectEditV2(
1452
+ application,
1453
+ project,
1454
+ plan,
1455
+ edit,
1456
+ metadataBinding,
1457
+ updatedAt,
1458
+ false,
1459
+ );
1460
+ }
1461
+ default: {
1462
+ const exhaustive: never = phase;
1463
+ return exhaustive;
1464
+ }
1465
+ }
1466
+ }
1467
+
1468
+ interface ProjectEditsTransactionInput<Edit> {
1469
+ readonly batch: {
1470
+ readonly ordered: readonly Edit[];
1471
+ };
1472
+ readonly basis: ProjectEditBasis;
1473
+ readonly project: string;
1474
+ readonly updatedAt?: string | undefined;
1475
+ }
1476
+
1477
+ interface CompleteProjectEditBindingsV3 {
1478
+ readonly manualZoomBindings: ReadonlyMap<
1479
+ string,
1480
+ ProjectCommitManualZoomBindingV3
1481
+ >;
1482
+ readonly metadataBinding: ProjectCommitMetadataBindingV2 | undefined;
1483
+ readonly metadataEdit: Extract<
1484
+ OrderedProjectEditV3,
1485
+ { readonly kind: "set-metadata-effects" }
1486
+ > | undefined;
1487
+ readonly requiresExternalRevalidation: boolean;
1488
+ readonly zooms: readonly ManualProjectZoomInputV3[];
1489
+ }
1490
+
1491
+ function completeProjectEditBindingsV3(input: {
1492
+ readonly batch: CommitProjectEditsInputV3["batch"];
1493
+ readonly manualZoomBindings?: readonly ProjectCommitManualZoomBindingV3[];
1494
+ readonly metadataBinding?: ProjectCommitMetadataBindingV2;
1495
+ }): CompleteProjectEditBindingsV3 {
1496
+ const metadataEdit = input.batch.ordered.find(
1497
+ (
1498
+ candidate,
1499
+ ): candidate is Extract<
1500
+ OrderedProjectEditV3,
1501
+ { readonly kind: "set-metadata-effects" }
1502
+ > => candidate.kind === "set-metadata-effects",
1503
+ );
1504
+ const requiresMetadataBinding = metadataEdit !== undefined
1505
+ && metadataEffectsAreEnabled(metadataEdit);
1506
+ if (requiresMetadataBinding && input.metadataBinding === undefined) {
1507
+ throw new ApplicationError(
1508
+ "incompatible",
1509
+ "Enabled metadata effects require a host-bound recording manifest.",
1510
+ );
1511
+ }
1512
+ if (!requiresMetadataBinding && input.metadataBinding !== undefined) {
1513
+ throw new ApplicationError(
1514
+ "incompatible",
1515
+ "A metadata binding is valid only for enabled metadata effects.",
1516
+ );
1517
+ }
1518
+ const zooms = manualZoomInputs(input.batch.ordered);
1519
+ const manualZoomBindings = indexManualZoomBindings(
1520
+ input.manualZoomBindings ?? [],
1521
+ );
1522
+ const expectedZoomIds = new Set(zooms.map(zoom => zoom.zoomId));
1523
+ const actualZoomIds = new Set(manualZoomBindings.keys());
1524
+ if (
1525
+ expectedZoomIds.size !== actualZoomIds.size
1526
+ || [...expectedZoomIds].some(zoomId => !actualZoomIds.has(zoomId))
1527
+ ) {
1528
+ throw new ApplicationError(
1529
+ "incompatible",
1530
+ "Manual zooms require exact host-owned recording bindings.",
1531
+ );
1532
+ }
1533
+ return {
1534
+ manualZoomBindings,
1535
+ metadataBinding: input.metadataBinding,
1536
+ metadataEdit,
1537
+ requiresExternalRevalidation: requiresMetadataBinding || zooms.length > 0,
1538
+ zooms,
1539
+ };
1540
+ }
1541
+
1542
+ async function deriveProjectEdits<Edit extends { readonly kind: string }>(
1543
+ input: {
1544
+ readonly abortSignal: AbortSignal;
1545
+ readonly edits: readonly Edit[];
1546
+ readonly plan: ProjectEditPlanV1;
1547
+ readonly project: VideoProjectV1;
1548
+ readonly updatedAt: string;
1549
+ },
1550
+ apply: (
1551
+ project: VideoProjectV1,
1552
+ plan: ProjectEditPlanV1,
1553
+ edit: Edit,
1554
+ updatedAt: string,
1555
+ ) => ProjectEditPlanV1 | Promise<ProjectEditPlanV1>,
1556
+ normalizationPhase?: (edit: Edit) => string,
1557
+ applyPhase?: (
1558
+ project: VideoProjectV1,
1559
+ plan: ProjectEditPlanV1,
1560
+ edits: readonly Edit[],
1561
+ updatedAt: string,
1562
+ abortSignal: AbortSignal,
1563
+ ) => ProjectEditPlanV1 | Promise<ProjectEditPlanV1>,
1564
+ ): Promise<ProjectEditPlanV1> {
1565
+ let next = input.plan;
1566
+ if (normalizationPhase !== undefined && applyPhase !== undefined) {
1567
+ let index = 0;
1568
+ while (index < input.edits.length) {
1569
+ throwIfAborted(input.abortSignal);
1570
+ const phase = normalizationPhase(input.edits[index]!);
1571
+ let endIndex = index + 1;
1572
+ while (
1573
+ endIndex < input.edits.length
1574
+ && normalizationPhase(input.edits[endIndex]!) === phase
1575
+ ) {
1576
+ endIndex += 1;
1577
+ }
1578
+ next = await applyPhase(
1579
+ input.project,
1580
+ next,
1581
+ input.edits.slice(index, endIndex),
1582
+ input.updatedAt,
1583
+ input.abortSignal,
1584
+ );
1585
+ next = normalizeCompleteEditDraft(next);
1586
+ throwIfAborted(input.abortSignal);
1587
+ index = endIndex;
1588
+ }
1589
+ return next;
1590
+ }
1591
+ for (const edit of input.edits) {
1592
+ throwIfAborted(input.abortSignal);
1593
+ next = await apply(input.project, next, edit, input.updatedAt);
1594
+ }
1595
+ return next;
1596
+ }
1597
+
1598
+ /**
1599
+ * Derive a complete V3 edit batch from exact frozen documents and complete
1600
+ * host bindings without publishing the mutable current-plan pointer.
1601
+ */
1602
+ export async function deriveProjectEditPlanV3(input: {
1603
+ readonly abortSignal: AbortSignal;
1604
+ readonly application: ApplicationContext;
1605
+ readonly batch: CommitProjectEditsInputV3["batch"];
1606
+ readonly manualZoomBindings?: readonly ProjectCommitManualZoomBindingV3[];
1607
+ readonly metadataBinding?: ProjectCommitMetadataBindingV2;
1608
+ readonly plan: ProjectEditPlanV1;
1609
+ readonly project: VideoProjectV1;
1610
+ readonly updatedAt: string;
1611
+ }): Promise<ProjectEditPlanV1> {
1612
+ const bindings = completeProjectEditBindingsV3(input);
1613
+ return await deriveProjectEdits(
1614
+ {
1615
+ abortSignal: input.abortSignal,
1616
+ edits: input.batch.ordered,
1617
+ plan: input.plan,
1618
+ project: input.project,
1619
+ updatedAt: input.updatedAt,
1620
+ },
1621
+ async (project, plan, edit, updatedAt) => (
1622
+ await applyOrderedProjectEditV3(
1623
+ input.application,
1624
+ project,
1625
+ plan,
1626
+ edit,
1627
+ bindings.metadataBinding,
1628
+ bindings.manualZoomBindings,
1629
+ updatedAt,
1630
+ false,
1631
+ )
1632
+ ),
1633
+ projectEditNormalizationPhaseV3,
1634
+ async (project, plan, edits, updatedAt, abortSignal) => (
1635
+ await applyOrderedProjectEditPhaseV3(
1636
+ input.application,
1637
+ project,
1638
+ plan,
1639
+ edits,
1640
+ bindings.metadataBinding,
1641
+ bindings.manualZoomBindings,
1642
+ updatedAt,
1643
+ abortSignal,
1644
+ )
1645
+ ),
1646
+ );
1647
+ }
1648
+
1649
+ /** Reverify every external recording fact bound into a complete V3 batch. */
1650
+ export async function revalidateProjectEditBindingsV3(input: {
1651
+ readonly abortSignal: AbortSignal;
1652
+ readonly application: ApplicationContext;
1653
+ readonly batch: CommitProjectEditsInputV3["batch"];
1654
+ readonly manualZoomBindings?: readonly ProjectCommitManualZoomBindingV3[];
1655
+ readonly metadataBinding?: ProjectCommitMetadataBindingV2;
1656
+ readonly project: VideoProjectV1;
1657
+ }): Promise<void> {
1658
+ const bindings = completeProjectEditBindingsV3(input);
1659
+ if (bindings.metadataEdit !== undefined && bindings.metadataBinding !== undefined) {
1660
+ await assertMetadataEffectsAvailable(
1661
+ input.application,
1662
+ input.project,
1663
+ bindings.metadataEdit,
1664
+ bindings.metadataBinding,
1665
+ );
1666
+ }
1667
+ const resolution = manualZoomResolutionContext(input.project);
1668
+ for (const zoom of bindings.zooms) {
1669
+ throwIfAborted(input.abortSignal);
1670
+ const expected = manualZoomBindingFor(
1671
+ bindings.manualZoomBindings,
1672
+ zoom.zoomId,
1673
+ );
1674
+ await resolveManualZoomBinding(
1675
+ input.application,
1676
+ input.project,
1677
+ resolution,
1678
+ zoom,
1679
+ expected.placementId,
1680
+ expected,
1681
+ );
1682
+ }
1683
+ }
1684
+
1685
+ async function executeProjectEditsTransaction<
1686
+ Edit extends { readonly kind: string },
1687
+ >(
1688
+ context: OperationExecutionContext,
1689
+ input: ProjectEditsTransactionInput<Edit>,
1690
+ apply: (
1691
+ project: VideoProjectV1,
1692
+ plan: ProjectEditPlanV1,
1693
+ edit: Edit,
1694
+ updatedAt: string,
1695
+ ) => ProjectEditPlanV1 | Promise<ProjectEditPlanV1>,
1696
+ revalidateBeforePublication?: (
1697
+ project: VideoProjectV1,
1698
+ ) => Promise<void>,
1699
+ normalizationPhase?: (edit: Edit) => string,
1700
+ applyPhase?: (
1701
+ project: VideoProjectV1,
1702
+ plan: ProjectEditPlanV1,
1703
+ edits: readonly Edit[],
1704
+ updatedAt: string,
1705
+ abortSignal: AbortSignal,
1706
+ ) => ProjectEditPlanV1 | Promise<ProjectEditPlanV1>,
1707
+ ): Promise<CommitProjectEditsOutput> {
1708
+ throwIfAborted(context.abortSignal);
1709
+ const snapshot = await openProjectSnapshot(
1710
+ context.application.paths.projectRoot,
1711
+ input.project,
1712
+ );
1713
+ assertProjectEditBasis(input.basis, snapshot);
1714
+ if (
1715
+ snapshot.plan.projectStructureSha256
1716
+ !== hashProjectStructure(snapshot.project)
1717
+ ) {
1718
+ throw new ApplicationError(
1719
+ "conflict",
1720
+ "The current edit plan belongs to an earlier project structure.",
1721
+ );
1722
+ }
1723
+ const updatedAt = input.updatedAt ?? IsoTimestampSchema.parse(
1724
+ context.application.clock.now().toISOString(),
1725
+ );
1726
+ const next = await deriveProjectEdits({
1727
+ abortSignal: context.abortSignal,
1728
+ edits: input.batch.ordered,
1729
+ plan: snapshot.plan,
1730
+ project: snapshot.project,
1731
+ updatedAt,
1732
+ }, apply, normalizationPhase, applyPhase);
1733
+ throwIfAborted(context.abortSignal);
1734
+
1735
+ // The scheduler owns the outer physical project lease. The transaction
1736
+ // marker makes process loss before/after the atomic current-plan
1737
+ // replacement explicitly recoverable on resume.
1738
+ // Reject already-stale external evidence before the workflow marks
1739
+ // publication as potentially authoritative. This avoids turning an
1740
+ // ordinary stale input into a reconciliation loop.
1741
+ await revalidateBeforePublication?.(snapshot.project);
1742
+ throwIfAborted(context.abortSignal);
1743
+ await context.workflow?.beforePublication();
1744
+ throwIfAborted(context.abortSignal);
1745
+ // External recording evidence can change while the scheduler fence is
1746
+ // checked. Reopen it after that awaited gate, then reassert the durable
1747
+ // workflow fence so cancellation/fence ownership is the final external
1748
+ // check before the project transaction publishes. Stopped recording
1749
+ // evidence is immutable by contract; the exact hash check still rejects
1750
+ // observable drift injected during the first gate.
1751
+ if (revalidateBeforePublication !== undefined) {
1752
+ await revalidateBeforePublication(snapshot.project);
1753
+ throwIfAborted(context.abortSignal);
1754
+ await context.workflow?.beforePublication();
1755
+ throwIfAborted(context.abortSignal);
1756
+ }
1757
+ const persistedProject = canonicalSlopcameraPersistenceDocument(snapshot.project);
1758
+ const persistedPriorPlan = canonicalSlopcameraPersistenceDocument(snapshot.plan);
1759
+ const persistedNextPlan = canonicalSlopcameraPersistenceDocument(next);
1760
+ const nodePlanSha256 = context.workflow?.nodePlanSha256
1761
+ ?? canonicalJsonSha256({
1762
+ generation: snapshot.generation,
1763
+ input,
1764
+ nextPlanSha256: hashProjectEditPlan(next),
1765
+ });
1766
+ await commitProjectStateTransaction({
1767
+ after: { plan: persistedNextPlan, project: persistedProject },
1768
+ before: { plan: persistedPriorPlan, project: persistedProject },
1769
+ fileSystem: snapshot.openProject.fileSystem,
1770
+ transactionId: projectEditTransactionId(nodePlanSha256),
1771
+ });
1772
+ return projectEditCommitReceipt(input, persistedProject, persistedNextPlan);
1773
+ }
1774
+
1775
+ function normalizeCompleteEditDraft(
1776
+ plan: ProjectEditPlanV1,
1777
+ ): ProjectEditPlanV1 {
1778
+ try {
1779
+ return normalizeProjectEditPlan(plan);
1780
+ } catch (error) {
1781
+ if (error instanceof ApplicationError) throw error;
1782
+ throw new ApplicationError(
1783
+ "conflict",
1784
+ "Complete edits are incompatible with the current project edit plan.",
1785
+ );
1786
+ }
1787
+ }
1788
+
1789
+ export const commitProjectEditsOperationDefinition = {
1790
+ inputSchema: CommitProjectEditsInputSchema,
1791
+ inputSchemaId: "slopcamera.operation.project.commit-edits.input/v1",
1792
+ kind: "project.commit-edits",
1793
+ lifecycle: {
1794
+ kind: "project-transaction",
1795
+ execute: async (context, input) => await executeProjectEditsTransaction(
1796
+ context,
1797
+ input,
1798
+ applyOrderedProjectEdit,
1799
+ ),
1800
+ },
1801
+ outputSchema: ProjectEditCommitReceiptSchema,
1802
+ outputSchemaId: "slopcamera.operation.project.commit-edits.output/v1",
1803
+ policy: {
1804
+ cache: "none",
1805
+ cancellable: true,
1806
+ effect: "project-mutation",
1807
+ maxDurationMs: 30_000,
1808
+ maxFanOut: 0,
1809
+ maxInputBytes: 16 * 1024 * 1024,
1810
+ maxOutputBytes: 4_096,
1811
+ preparation: ["project-state"],
1812
+ resources: [
1813
+ { amount: 1, resource: "cpu" },
1814
+ { amount: 1, resource: "local-io" },
1815
+ { amount: 1, resource: "project-publication" },
1816
+ ],
1817
+ resume: "recoverable-transaction",
1818
+ },
1819
+ summarize: output => ({
1820
+ fields: {
1821
+ generationSha256: output.generation.generationSha256,
1822
+ operation: output.operation,
1823
+ planHash: output.planHash,
1824
+ planId: output.planId,
1825
+ projectId: output.projectId,
1826
+ },
1827
+ kind: "project.commit-edits",
1828
+ }),
1829
+ version: 1,
1830
+ } satisfies OperationDefinition<
1831
+ "project.commit-edits",
1832
+ CommitProjectEditsInput,
1833
+ CommitProjectEditsOutput
1834
+ >;
1835
+
1836
+ export const commitProjectEditsOperationDefinitionV2 = {
1837
+ inputSchema: CommitProjectEditsInputV2Schema,
1838
+ inputSchemaId: "slopcamera.operation.project.commit-edits.input/v2",
1839
+ kind: "project.commit-edits",
1840
+ lifecycle: {
1841
+ kind: "project-transaction",
1842
+ execute: async (context, input) => {
1843
+ const metadataEdit = input.batch.ordered.find(
1844
+ (
1845
+ candidate,
1846
+ ): candidate is Extract<
1847
+ OrderedProjectEditV2,
1848
+ { readonly kind: "set-metadata-effects" }
1849
+ > => candidate.kind === "set-metadata-effects",
1850
+ );
1851
+ const requiresMetadataBinding = metadataEdit !== undefined
1852
+ && metadataEffectsAreEnabled(metadataEdit);
1853
+ if (requiresMetadataBinding && input.metadataBinding === undefined) {
1854
+ throw new ApplicationError(
1855
+ "incompatible",
1856
+ "Enabled metadata effects require a host-bound recording manifest.",
1857
+ );
1858
+ }
1859
+ return await executeProjectEditsTransaction(
1860
+ context,
1861
+ input,
1862
+ async (project, plan, edit, updatedAt) => (
1863
+ await applyOrderedProjectEditV2(
1864
+ context.application,
1865
+ project,
1866
+ plan,
1867
+ edit,
1868
+ input.metadataBinding,
1869
+ updatedAt,
1870
+ )
1871
+ ),
1872
+ requiresMetadataBinding
1873
+ ? async (project) => {
1874
+ await assertMetadataEffectsAvailable(
1875
+ context.application,
1876
+ project,
1877
+ metadataEdit,
1878
+ input.metadataBinding,
1879
+ );
1880
+ }
1881
+ : undefined,
1882
+ );
1883
+ },
1884
+ },
1885
+ outputSchema: ProjectEditCommitReceiptSchema,
1886
+ outputSchemaId: "slopcamera.operation.project.commit-edits.output/v2",
1887
+ policy: {
1888
+ cache: "none",
1889
+ cancellable: true,
1890
+ effect: "project-mutation",
1891
+ maxDurationMs: 30_000,
1892
+ maxFanOut: 0,
1893
+ maxInputBytes: 16 * 1024 * 1024,
1894
+ maxOutputBytes: 4_096,
1895
+ preparation: ["project-state", "recording-metadata"],
1896
+ resources: [
1897
+ { amount: 1, resource: "cpu" },
1898
+ { amount: 1, resource: "local-io" },
1899
+ { amount: 1, resource: "project-publication" },
1900
+ ],
1901
+ resume: "recoverable-transaction",
1902
+ },
1903
+ summarize: output => ({
1904
+ fields: {
1905
+ generationSha256: output.generation.generationSha256,
1906
+ operation: output.operation,
1907
+ planHash: output.planHash,
1908
+ planId: output.planId,
1909
+ projectId: output.projectId,
1910
+ },
1911
+ kind: "project.commit-edits",
1912
+ }),
1913
+ version: 2,
1914
+ } satisfies OperationDefinition<
1915
+ "project.commit-edits",
1916
+ CommitProjectEditsInputV2,
1917
+ CommitProjectEditsOutput
1918
+ >;
1919
+
1920
+ export const commitProjectEditsOperationDefinitionV3 = {
1921
+ inputSchema: CommitProjectEditsInputV3Schema,
1922
+ inputSchemaId: "slopcamera.operation.project.commit-edits.input/v3",
1923
+ kind: "project.commit-edits",
1924
+ lifecycle: {
1925
+ kind: "project-transaction",
1926
+ execute: async (context, input) => {
1927
+ const metadataEdit = input.batch.ordered.find(
1928
+ (
1929
+ candidate,
1930
+ ): candidate is Extract<
1931
+ OrderedProjectEditV3,
1932
+ { readonly kind: "set-metadata-effects" }
1933
+ > => candidate.kind === "set-metadata-effects",
1934
+ );
1935
+ const requiresMetadataBinding = metadataEdit !== undefined
1936
+ && metadataEffectsAreEnabled(metadataEdit);
1937
+ if (requiresMetadataBinding && input.metadataBinding === undefined) {
1938
+ throw new ApplicationError(
1939
+ "incompatible",
1940
+ "Enabled metadata effects require a host-bound recording manifest.",
1941
+ );
1942
+ }
1943
+ const zooms = manualZoomInputs(input.batch.ordered);
1944
+ const manualZoomBindings = indexManualZoomBindings(
1945
+ input.manualZoomBindings ?? [],
1946
+ );
1947
+ const expectedZoomIds = new Set(zooms.map(zoom => zoom.zoomId));
1948
+ const actualZoomIds = new Set(manualZoomBindings.keys());
1949
+ if (
1950
+ expectedZoomIds.size !== actualZoomIds.size
1951
+ || [...expectedZoomIds].some(zoomId => !actualZoomIds.has(zoomId))
1952
+ ) {
1953
+ throw new ApplicationError(
1954
+ "incompatible",
1955
+ "Manual zooms require exact host-owned recording bindings.",
1956
+ );
1957
+ }
1958
+ return await executeProjectEditsTransaction(
1959
+ context,
1960
+ input,
1961
+ async (project, plan, edit, updatedAt) => (
1962
+ await applyOrderedProjectEditV3(
1963
+ context.application,
1964
+ project,
1965
+ plan,
1966
+ edit,
1967
+ input.metadataBinding,
1968
+ manualZoomBindings,
1969
+ updatedAt,
1970
+ false,
1971
+ )
1972
+ ),
1973
+ requiresMetadataBinding || zooms.length > 0
1974
+ ? async (project) => {
1975
+ if (requiresMetadataBinding) {
1976
+ await assertMetadataEffectsAvailable(
1977
+ context.application,
1978
+ project,
1979
+ metadataEdit,
1980
+ input.metadataBinding,
1981
+ );
1982
+ }
1983
+ const resolution = manualZoomResolutionContext(project);
1984
+ for (const zoom of zooms) {
1985
+ throwIfAborted(context.abortSignal);
1986
+ const expected = manualZoomBindingFor(
1987
+ manualZoomBindings,
1988
+ zoom.zoomId,
1989
+ );
1990
+ await resolveManualZoomBinding(
1991
+ context.application,
1992
+ project,
1993
+ resolution,
1994
+ zoom,
1995
+ expected.placementId,
1996
+ expected,
1997
+ );
1998
+ }
1999
+ }
2000
+ : undefined,
2001
+ projectEditNormalizationPhaseV3,
2002
+ async (
2003
+ project,
2004
+ plan,
2005
+ edits,
2006
+ updatedAt,
2007
+ abortSignal,
2008
+ ) => await applyOrderedProjectEditPhaseV3(
2009
+ context.application,
2010
+ project,
2011
+ plan,
2012
+ edits,
2013
+ input.metadataBinding,
2014
+ manualZoomBindings,
2015
+ updatedAt,
2016
+ abortSignal,
2017
+ ),
2018
+ );
2019
+ },
2020
+ },
2021
+ outputSchema: ProjectEditCommitReceiptSchema,
2022
+ outputSchemaId: "slopcamera.operation.project.commit-edits.output/v3",
2023
+ policy: {
2024
+ cache: "none",
2025
+ cancellable: true,
2026
+ effect: "project-mutation",
2027
+ maxDurationMs: 90_000,
2028
+ maxFanOut: 0,
2029
+ maxInputBytes: 16 * 1024 * 1024,
2030
+ maxOutputBytes: 4_096,
2031
+ preparation: ["project-state", "recording-metadata"],
2032
+ resources: [
2033
+ { amount: 1, resource: "cpu" },
2034
+ { amount: 1, resource: "local-io" },
2035
+ { amount: 1, resource: "project-publication" },
2036
+ ],
2037
+ resume: "recoverable-transaction",
2038
+ },
2039
+ summarize: output => ({
2040
+ fields: {
2041
+ generationSha256: output.generation.generationSha256,
2042
+ operation: output.operation,
2043
+ planHash: output.planHash,
2044
+ planId: output.planId,
2045
+ projectId: output.projectId,
2046
+ },
2047
+ kind: "project.commit-edits",
2048
+ }),
2049
+ version: 3,
2050
+ } satisfies OperationDefinition<
2051
+ "project.commit-edits",
2052
+ CommitProjectEditsInputV3,
2053
+ CommitProjectEditsOutput
2054
+ >;