@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,131 @@
1
+ import { deepFreezeJson } from "../code/json-snapshot.js"
2
+ import { SpatialScenePatchV1Schema, type SpatialEntity, type SpatialSceneV1 } from "./contracts.js"
3
+ import { applySpatialEntityOverride } from "./evaluate.js"
4
+ import { parseSpatialScene, parseSpatialValue, SpatialSceneError, spatialAssetClosureDigests, spatialValueSha256 } from "./identity.js"
5
+
6
+ export interface SpatialSceneDiffEntry {
7
+ readonly kind: "added" | "removed" | "changed"
8
+ readonly collection: "entities" | "cameras" | "animations" | "generators" | "overrides" | "assets"
9
+ readonly id: string
10
+ readonly properties: readonly string[]
11
+ }
12
+ export interface SpatialScenePatchResult {
13
+ readonly scene: SpatialSceneV1
14
+ readonly sceneSha256: string
15
+ readonly diff: readonly SpatialSceneDiffEntry[]
16
+ }
17
+
18
+ function diffCollection<T extends object>(collection: SpatialSceneDiffEntry["collection"], before: readonly T[], after: readonly T[], identify: (item: T) => string): SpatialSceneDiffEntry[] {
19
+ const old = new Map(before.map(item => [identify(item), item]))
20
+ const current = new Map(after.map(item => [identify(item), item]))
21
+ const result: SpatialSceneDiffEntry[] = []
22
+ for (const id of [...new Set([...old.keys(), ...current.keys()])].sort()) {
23
+ const a = old.get(id), b = current.get(id)
24
+ if (a === undefined || b === undefined) {
25
+ result.push({ kind: a === undefined ? "added" : "removed", collection, id, properties: Object.keys(a ?? b!).sort() })
26
+ } else {
27
+ const aRecord = a as Record<string, unknown>, bRecord = b as Record<string, unknown>
28
+ const properties = [...new Set([...Object.keys(a), ...Object.keys(b)])].sort().filter(key =>
29
+ spatialValueSha256(Object.hasOwn(a, key) ? { value: aRecord[key] } : {}) !== spatialValueSha256(Object.hasOwn(b, key) ? { value: bRecord[key] } : {}))
30
+ if (properties.length) result.push({ kind: "changed", collection, id, properties })
31
+ }
32
+ }
33
+ return result
34
+ }
35
+
36
+ /** Atomic in-memory transaction. Final closure is checked once, before publishing any result. */
37
+ export function applySpatialScenePatch(sceneInput: unknown, patchInput: unknown): SpatialScenePatchResult {
38
+ const original = parseSpatialScene(sceneInput)
39
+ const patch = parseSpatialValue(SpatialScenePatchV1Schema, patchInput, "scene patch")
40
+ if (patch.expectedSceneSha256 !== spatialValueSha256(original)) throw new SpatialSceneError("conflict", "Scene revision changed; inspect and rebase the patch.", "expectedSceneSha256")
41
+ const entities = new Map(original.entities.map(entity => [entity.entityId, entity]))
42
+ const assets = new Map(original.assets.map(asset => [asset.assetId, asset]))
43
+ const addressedGeometry = new Set<string>(), replacedGenerators = new Set<string>()
44
+ const cameras = new Map(original.cameras.map(camera => [camera.cameraId, camera]))
45
+ const animations = new Map(original.animations.map(channel => [channel.channelId, channel]))
46
+ const generators = new Map(original.generators.map(generator => [generator.generatorId, generator]))
47
+ const overrides = new Map(original.overrides.map(override => [`${override.entityId}:${override.property}`, override]))
48
+ function authored(id: string): SpatialEntity {
49
+ const entity = entities.get(id)
50
+ if (!entity) throw new SpatialSceneError("not-found", `Entity ${id} does not exist.`, "operations")
51
+ if (entity.origin.kind !== "authored") throw new SpatialSceneError("conflict", "Generated entities can be edited only through declared overrides or retained output replacement.", "operations")
52
+ return entity
53
+ }
54
+ for (const operation of patch.operations) {
55
+ switch (operation.kind) {
56
+ case "add-asset":
57
+ if (assets.has(operation.asset.assetId)) throw new SpatialSceneError("conflict", `Asset ${operation.asset.assetId} already exists.`)
58
+ assets.set(operation.asset.assetId, operation.asset)
59
+ break
60
+ case "replace-asset":
61
+ if (!assets.has(operation.asset.assetId)) throw new SpatialSceneError("not-found", `Asset ${operation.asset.assetId} does not exist.`)
62
+ assets.set(operation.asset.assetId, operation.asset)
63
+ break
64
+ case "set-mesh-geometry": {
65
+ const entity = authored(operation.entityId)
66
+ if (entity.kind !== "mesh") throw new SpatialSceneError("conflict", "Geometry replacement requires an authored mesh wrapper.")
67
+ entities.set(operation.entityId, { ...entity, geometry: operation.geometry })
68
+ addressedGeometry.add(operation.entityId)
69
+ break
70
+ }
71
+ case "rename-entity": entities.set(operation.entityId, { ...authored(operation.entityId), name: operation.name }); break
72
+ case "reparent-entity": entities.set(operation.entityId, { ...authored(operation.entityId), parentId: operation.parentId }); break
73
+ case "set-transform": entities.set(operation.entityId, { ...authored(operation.entityId), transform: operation.transform }); break
74
+ case "set-color": entities.set(operation.entityId, applySpatialEntityOverride(authored(operation.entityId), { entityId: operation.entityId, property: "color", value: operation.color })); break
75
+ case "set-opacity": entities.set(operation.entityId, applySpatialEntityOverride(authored(operation.entityId), { entityId: operation.entityId, property: "opacity", value: operation.opacity })); break
76
+ case "set-camera": cameras.set(operation.camera.cameraId, operation.camera); break
77
+ case "set-channel": animations.set(operation.channel.channelId, operation.channel); break
78
+ case "remove-channel":
79
+ if (!animations.delete(operation.channelId)) throw new SpatialSceneError("not-found", `Channel ${operation.channelId} does not exist.`)
80
+ break
81
+ case "add-entity":
82
+ if (entities.has(operation.entity.entityId)) throw new SpatialSceneError("conflict", `Entity ${operation.entity.entityId} already exists.`)
83
+ if (operation.entity.origin.kind !== "authored") throw new SpatialSceneError("conflict", "Add generated entities through retained generator output replacement.")
84
+ entities.set(operation.entity.entityId, operation.entity)
85
+ if (operation.entity.kind === "mesh") addressedGeometry.add(operation.entity.entityId)
86
+ break
87
+ case "remove-entity": authored(operation.entityId); entities.delete(operation.entityId); break
88
+ case "set-override": overrides.set(`${operation.override.entityId}:${operation.override.property}`, operation.override); break
89
+ case "remove-override":
90
+ if (!overrides.delete(`${operation.entityId}:${operation.property}`)) throw new SpatialSceneError("not-found", "Override does not exist.")
91
+ break
92
+ case "replace-generator-output": {
93
+ const generatorId = operation.generator.generatorId
94
+ replacedGenerators.add(generatorId)
95
+ for (const entity of entities.values()) if (entity.origin.kind === "generated" && entity.origin.generatorId === generatorId) entities.delete(entity.entityId)
96
+ for (const entity of operation.entities) {
97
+ if (entity.origin.kind !== "generated" || entity.origin.generatorId !== generatorId) throw new SpatialSceneError("conflict", "Generator replacement must contain only its own retained output.")
98
+ if (entities.has(entity.entityId)) throw new SpatialSceneError("conflict", `Generator output collides with ${entity.entityId}.`)
99
+ entities.set(entity.entityId, entity)
100
+ }
101
+ generators.set(generatorId, operation.generator)
102
+ break
103
+ }
104
+ }
105
+ }
106
+ const scene = parseSpatialScene({ ...original, assets: [...assets.values()], entities: [...entities.values()], cameras: [...cameras.values()], animations: [...animations.values()], generators: [...generators.values()], overrides: [...overrides.values()] })
107
+ const beforeAssets = new Map(original.assets.map(asset => [asset.assetId, asset]))
108
+ const oldClosure = spatialAssetClosureDigests(original.assets), newClosure = spatialAssetClosureDigests(scene.assets)
109
+ for (const entity of scene.entities) {
110
+ const assetId = entity.kind === "mesh" && entity.geometry.kind === "asset" ? entity.geometry.assetId
111
+ : entity.kind === "text" ? entity.fontAssetId : "assetId" in entity ? entity.assetId : undefined
112
+ if (assetId === undefined) continue
113
+ if (entity.origin.kind === "generated" && oldClosure[assetId] !== undefined && oldClosure[assetId] !== newClosure[assetId] && !replacedGenerators.has(entity.origin.generatorId)) {
114
+ throw new SpatialSceneError("conflict", "Changing a generated part's asset closure requires explicit retained generator output replacement.")
115
+ }
116
+ if (entity.kind === "mesh" && entity.geometry.kind === "asset" && (entity.geometry.nodeIndex !== undefined || entity.geometry.clip !== undefined)
117
+ && beforeAssets.has(assetId) && beforeAssets.get(assetId)!.payload.sha256 !== assets.get(assetId)!.payload.sha256
118
+ && !addressedGeometry.has(entity.entityId) && !(entity.origin.kind === "generated" && replacedGenerators.has(entity.origin.generatorId))) {
119
+ throw new SpatialSceneError("conflict", "Replacing addressed GLB bytes requires explicit set-mesh-geometry with the new local node/clip addresses; internal correspondence is not inferred.")
120
+ }
121
+ }
122
+ const diff = [
123
+ ...diffCollection("assets", original.assets, scene.assets, asset => asset.assetId),
124
+ ...diffCollection("entities", original.entities, scene.entities, entity => entity.entityId),
125
+ ...diffCollection("cameras", original.cameras, scene.cameras, camera => camera.cameraId),
126
+ ...diffCollection("animations", original.animations, scene.animations, channel => channel.channelId),
127
+ ...diffCollection("generators", original.generators, scene.generators, generator => generator.generatorId),
128
+ ...diffCollection("overrides", original.overrides, scene.overrides, override => `${override.entityId}:${override.property}`),
129
+ ]
130
+ return deepFreezeJson({ scene, sceneSha256: spatialValueSha256(scene), diff })
131
+ }
@@ -0,0 +1,53 @@
1
+ import { deepFreezeJson } from "../code/json-snapshot.js"
2
+ import { SpatialFrameRateSchema, SpatialTimeUsSchema, type SpatialFrameRate } from "./contracts.js"
3
+ import { parseSpatialValue, SpatialSceneError } from "./identity.js"
4
+
5
+ /** Decimal strings retain exact rational values across JSON and SDK boundaries. */
6
+ export interface SpatialRational { readonly numerator: string; readonly denominator: string }
7
+ export interface SpatialFrameSample { readonly frameIndex: number; readonly timeUs: number; readonly exactTimeUs: SpatialRational }
8
+
9
+ function gcd(a: bigint, b: bigint): bigint {
10
+ while (b !== 0n) { const remainder = a % b; a = b; b = remainder }
11
+ return a
12
+ }
13
+ function rational(numerator: bigint, denominator: bigint): SpatialRational {
14
+ const divisor = gcd(numerator, denominator)
15
+ return Object.freeze({ numerator: String(numerator / divisor), denominator: String(denominator / divisor) })
16
+ }
17
+ function integer(input: number, name: string): bigint {
18
+ if (!Number.isSafeInteger(input) || input < 0) throw new SpatialSceneError("invalid-data", `${name} must be a nonnegative safe integer.`)
19
+ return BigInt(input)
20
+ }
21
+ function duration(input: number): bigint {
22
+ const value = parseSpatialValue(SpatialTimeUsSchema, input, "durationUs")
23
+ if (value === 0) throw new SpatialSceneError("invalid-data", "Output duration must be positive.")
24
+ return BigInt(value)
25
+ }
26
+
27
+ export function reduceSpatialFrameRate(input: unknown): SpatialFrameRate {
28
+ const rate = parseSpatialValue(SpatialFrameRateSchema, input, "frame rate")
29
+ const divisor = Number(gcd(BigInt(rate.numerator), BigInt(rate.denominator)))
30
+ return Object.freeze({ numerator: rate.numerator / divisor, denominator: rate.denominator / divisor })
31
+ }
32
+
33
+ /** Number of nominal samples i/fps in the half-open authored duration. */
34
+ export function spatialFrameCount(durationUs: number, rateInput: unknown): number {
35
+ const end = duration(durationUs), rate = reduceSpatialFrameRate(rateInput)
36
+ const numerator = end * BigInt(rate.numerator), denominator = 1_000_000n * BigInt(rate.denominator)
37
+ return Number((numerator + denominator - 1n) / denominator)
38
+ }
39
+
40
+ /** Every sample is rounded independently to nearest microsecond, with ties toward +infinity. */
41
+ export function spatialFrameSample(frameIndex: number, durationUs: number, rateInput: unknown): SpatialFrameSample {
42
+ const index = integer(frameIndex, "frameIndex"), end = duration(durationUs), rate = reduceSpatialFrameRate(rateInput)
43
+ const numerator = index * 1_000_000n * BigInt(rate.denominator), denominator = BigInt(rate.numerator)
44
+ if (numerator >= end * denominator) throw new SpatialSceneError("invalid-data", "Frame lies outside the half-open duration before quantization.", "frameIndex")
45
+ const timeUs = Number((2n * numerator + denominator) / (2n * denominator))
46
+ return deepFreezeJson({ frameIndex, timeUs, exactTimeUs: rational(numerator, denominator) })
47
+ }
48
+
49
+ /** Encoded duration is frameCount/fps and can exceed authored duration by less than one frame. */
50
+ export function spatialOutputDuration(durationUs: number, rateInput: unknown): SpatialRational {
51
+ const rate = reduceSpatialFrameRate(rateInput)
52
+ return rational(BigInt(spatialFrameCount(durationUs, rate)) * 1_000_000n * BigInt(rate.denominator), BigInt(rate.numerator))
53
+ }
@@ -0,0 +1,129 @@
1
+ import { z } from "zod"
2
+ import { createBoundedJsonValueSnapshot } from "../code/json-snapshot.js"
3
+ import { assertDistinctPaths, parseStudioValue, STUDIO_LIMITS, studioCompare, studioDocument, type StudioReadonly } from "./shared.js"
4
+
5
+ export { STUDIO_LIMITS } from "./shared.js"
6
+ export const StudioDigestSchema = z.string().regex(/^[a-f0-9]{64}$/u)
7
+ export const StudioEngineSchema = z.enum(["blender", "manim", "cadquery"])
8
+ export const StudioPathSchema = z.string().min(1).max(1024).refine(path =>
9
+ path.normalize("NFC") === path && !path.startsWith("/") && !/[\\:\u0000-\u001f\u007f]/u.test(path)
10
+ && path.split("/").every(part => part !== "" && part !== "." && part !== ".." && !/[. ]$/u.test(part)),
11
+ "Studio paths must be normalized, contained POSIX-relative names.")
12
+ const identifier = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]{0,127}$/u)
13
+ const stableId = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/u)
14
+ const sourceFile = z.strictObject({ path: StudioPathSchema, sha256: StudioDigestSchema, bytes: z.number().int().safe().nonnegative().max(STUDIO_LIMITS.sourceBytes) })
15
+ const sourceBundleShape = z.strictObject({
16
+ kind: z.literal("slopcamera.studio-source-bundle"), schemaVersion: z.literal(1), engine: StudioEngineSchema,
17
+ entrypoint: z.discriminatedUnion("kind", [z.strictObject({ kind: z.literal("python"), path: StudioPathSchema.refine(path => path.endsWith(".py")) }), z.strictObject({ kind: z.literal("blend"), path: StudioPathSchema.refine(path => path.endsWith(".blend")) })]),
18
+ files: z.array(sourceFile).min(1).max(STUDIO_LIMITS.sourceFiles),
19
+ }).superRefine((value, context) => {
20
+ const issue = (message: string) => context.addIssue({ code: "custom", message })
21
+ try { assertDistinctPaths(value.files.map(file => file.path)) } catch { issue("Source paths must be distinct and cannot have file ancestors.") }
22
+ if (value.files.reduce((total, file) => total + file.bytes, 0) > STUDIO_LIMITS.sourceBytes) issue("Source bundle exceeds four GiB.")
23
+ if (!value.files.some(file => file.path === value.entrypoint.path && file.bytes > 0)) issue("Entrypoint must name a declared nonempty source file.")
24
+ if (value.entrypoint.kind === "blend" && value.engine !== "blender") issue("Blend entrypoints require Blender.")
25
+ })
26
+ const sourceBundle = sourceBundleShape.transform(value => ({ ...value, files: [...value.files].sort((a, b) => studioCompare(a.path, b.path)) })).pipe(sourceBundleShape)
27
+ export const StudioSourceBundleSchema = studioDocument(sourceBundle, "studio source bundle")
28
+ export type StudioSourceBundle = StudioReadonly<z.infer<typeof StudioSourceBundleSchema>>
29
+ export const parseStudioSourceBundle = (input: unknown): StudioSourceBundle => parseStudioValue(StudioSourceBundleSchema, input)
30
+
31
+ export const StudioSourceSpaceSchema = z.strictObject({ units: z.enum(["meters", "millimeters", "centimeters"]), upAxis: z.enum(["x", "y", "z"]), handedness: z.enum(["right", "left"]) })
32
+ export const StudioOutputRoleSchema = z.enum(["native-source", "model", "beauty", "auxiliary", "simulation-cache", "audio"])
33
+ export const StudioOutputFormatSchema = z.enum(["py", "blend", "usd", "usda", "usdc", "glb", "step", "png", "exr", "mp4", "mov", "webm", "wav", "flac", "mp3", "cache"])
34
+ const raster = z.strictObject({
35
+ kind: z.literal("raster"), colorSpace: z.enum(["srgb", "linear-rec709", "data", "unspecified"]), alpha: z.enum(["opaque", "straight", "premultiplied", "none"]),
36
+ dataType: z.enum(["uint8", "uint16", "float16", "float32"]), channels: z.array(z.string().regex(/^[A-Za-z][A-Za-z0-9_.-]{0,63}$/u)).min(1).max(32),
37
+ semantic: z.enum(["color", "depth", "normal", "object-id", "mask", "custom"]), unit: z.enum(["unitless", "meters", "millimeters", "centimeters"]),
38
+ }).superRefine((value, context) => {
39
+ if (new Set(value.channels).size !== value.channels.length) context.addIssue({ code: "custom", message: "Raster channels must be unique." })
40
+ if (value.semantic !== "color" && value.colorSpace !== "data") context.addIssue({ code: "custom", message: "Non-color raster passes must declare data color space." })
41
+ if (value.semantic === "color" && value.colorSpace === "data") context.addIssue({ code: "custom", message: "Color output cannot declare data color space." })
42
+ if (value.semantic !== "depth" && value.unit !== "unitless") context.addIssue({ code: "custom", message: "Only depth passes declare distance units." })
43
+ })
44
+ export const StudioOutputInterpretationSchema = z.discriminatedUnion("kind", [
45
+ raster, z.strictObject({ kind: z.literal("model"), sourceSpace: StudioSourceSpaceSchema }),
46
+ z.strictObject({ kind: z.literal("native-source") }), z.strictObject({ kind: z.literal("cache"), semantics: z.literal("opaque-native") }),
47
+ z.strictObject({ kind: z.literal("audio"), sampleRate: z.number().int().min(8000).max(384000), channels: z.number().int().min(1).max(32) }),
48
+ ])
49
+ const outputCommon = { id: stableId, role: StudioOutputRoleSchema, format: StudioOutputFormatSchema, interpretation: StudioOutputInterpretationSchema }
50
+ const pattern = z.string().max(1024).refine(value => value.split("%06d").length === 2 && !value.replace("%06d", "").includes("%") && StudioPathSchema.safeParse(value.replace("%06d", "000000")).success,
51
+ "Sequence paths require exactly one %06d placeholder in a safe relative path.")
52
+ function compatibleOutput(value: { role: z.infer<typeof StudioOutputRoleSchema>; format: z.infer<typeof StudioOutputFormatSchema>; interpretation: z.infer<typeof StudioOutputInterpretationSchema> }): boolean {
53
+ if (value.role === "native-source") return ["py", "blend"].includes(value.format) && value.interpretation.kind === "native-source"
54
+ if (value.role === "model") return ["blend", "usd", "usda", "usdc", "glb", "step"].includes(value.format) && value.interpretation.kind === "model"
55
+ if (value.role === "simulation-cache") return value.format === "cache" && value.interpretation.kind === "cache"
56
+ if (value.role === "audio") return ["wav", "flac", "mp3"].includes(value.format) && value.interpretation.kind === "audio"
57
+ return ["png", "exr", "mp4", "mov", "webm"].includes(value.format) && value.interpretation.kind === "raster" && (value.role !== "beauty" || value.interpretation.semantic === "color")
58
+ }
59
+ export const StudioOutputSpecSchema = z.discriminatedUnion("kind", [
60
+ z.strictObject({ ...outputCommon, kind: z.literal("file"), path: StudioPathSchema }),
61
+ z.strictObject({ ...outputCommon, kind: z.literal("sequence"), pathPattern: pattern }),
62
+ z.strictObject({ ...outputCommon, kind: z.literal("directory"), path: StudioPathSchema }),
63
+ ]).superRefine((value, context) => {
64
+ const issue = (message: string) => context.addIssue({ code: "custom", message })
65
+ if (!compatibleOutput(value)) issue("Output role, format, and interpretation disagree.")
66
+ if ((value.kind === "directory") !== (value.format === "cache")) issue("Only native cache outputs use directory declarations.")
67
+ if (value.kind === "sequence" && !["png", "exr"].includes(value.format)) issue("Numbered sequences support PNG or EXR only.")
68
+ if (value.kind !== "directory") {
69
+ const path = value.kind === "sequence" ? value.pathPattern : value.path
70
+ if (!path.endsWith(`.${value.format}`) && !(value.format === "step" && path.endsWith(".stp"))) issue("Output extension must agree with its declared format.")
71
+ }
72
+ if (value.interpretation.kind === "raster") {
73
+ if (value.format === "png" && !["uint8", "uint16"].includes(value.interpretation.dataType)) issue("PNG declares integer sample data.")
74
+ if (value.format === "exr" && !["float16", "float32"].includes(value.interpretation.dataType)) issue("The admitted EXR profile declares floating point sample data.")
75
+ }
76
+ })
77
+ export type StudioOutputSpec = StudioReadonly<z.infer<typeof StudioOutputSpecSchema>>
78
+ const frameRateShape = z.strictObject({ numerator: z.number().int().min(1).max(1_000_000), denominator: z.number().int().min(1).max(1_000_000) })
79
+ .refine(value => value.numerator / value.denominator <= 240, "Studio cadence exceeds 240 fps.")
80
+ const frameRate = frameRateShape.transform(value => { let a = value.numerator, b = value.denominator; while (b !== 0) { const remainder = a % b; a = b; b = remainder } return { numerator: value.numerator / a, denominator: value.denominator / a } }).pipe(frameRateShape)
81
+ export const StudioRenderSchema = z.strictObject({
82
+ width: z.number().int().min(1).max(STUDIO_LIMITS.dimension), height: z.number().int().min(1).max(STUDIO_LIMITS.dimension), frameRate,
83
+ startFrame: z.number().int().nonnegative().max(STUDIO_LIMITS.frameIndexExclusive - 1), endFrameExclusive: z.number().int().positive().max(STUDIO_LIMITS.frameIndexExclusive),
84
+ }).refine(value => value.endFrameExclusive > value.startFrame && value.endFrameExclusive - value.startFrame <= STUDIO_LIMITS.frames && value.width * value.height <= STUDIO_LIMITS.pixels,
85
+ "Render requires a bounded, nonempty half-open frame interval and pixel area.")
86
+ export const StudioEngineOptionsSchema = z.discriminatedUnion("engine", [
87
+ z.strictObject({ engine: z.literal("blender"), renderer: z.enum(["cycles", "eevee"]), device: z.enum(["cpu", "gpu"]), samples: z.number().int().min(1).max(4096), transparent: z.boolean(), viewTransform: z.enum(["AgX", "Standard"]), denoise: z.boolean(), seed: z.number().int().min(0).max(0xffff_ffff) }),
88
+ z.strictObject({ engine: z.literal("manim"), scene: identifier, renderer: z.literal("cairo"), transparent: z.boolean() }),
89
+ z.strictObject({ engine: z.literal("cadquery"), exportVariable: identifier, tolerance: z.number().finite().positive().max(1), angularTolerance: z.number().finite().positive().max(Math.PI) }),
90
+ ])
91
+ // Capture proves the full JSON closure; avoid a second recursive schema/type expansion.
92
+ const parameters = z.preprocess(value => value === undefined ? undefined : createBoundedJsonValueSnapshot(value, STUDIO_LIMITS.parameterBytes, "studio parameters", { maximumDepth: STUDIO_LIMITS.parameterDepth, maximumValues: STUDIO_LIMITS.parameterValues }).value, z.record(z.string(), z.unknown()))
93
+ export const StudioExecutionProfileSchema = z.strictObject({ trust: z.literal("trusted-current-user"), isolation: z.literal("none"), hermetic: z.literal(false) })
94
+ const jobShape = z.strictObject({
95
+ kind: z.literal("slopcamera.studio-job"), schemaVersion: z.literal(1), jobId: z.string().regex(/^studio_[a-zA-Z0-9][a-zA-Z0-9_-]{0,120}$/u), bundleSha256: StudioDigestSchema,
96
+ stage: z.enum(["build", "bake", "render"]), parameters, engine: StudioEngineOptionsSchema, render: StudioRenderSchema.optional(),
97
+ outputs: z.array(StudioOutputSpecSchema).min(1).max(STUDIO_LIMITS.outputSpecifications),
98
+ limits: z.strictObject({ timeoutSeconds: z.number().int().min(1).max(STUDIO_LIMITS.timeoutSeconds), maximumOutputBytes: z.number().int().safe().min(1).max(STUDIO_LIMITS.outputBytes), maximumOutputFiles: z.number().int().min(1).max(STUDIO_LIMITS.outputFiles) }),
99
+ execution: StudioExecutionProfileSchema,
100
+ }).superRefine((value, context) => {
101
+ if (value.render === undefined && (value.stage === "render" && value.engine.engine !== "cadquery" || value.outputs.some(output => output.kind === "sequence" || output.interpretation.kind === "raster"))) context.addIssue({ code: "custom", message: "Raster outputs, render stage, and numbered sequences require explicit dimensions and a frame interval." })
102
+ if (new Set(value.outputs.map(output => output.id)).size !== value.outputs.length) context.addIssue({ code: "custom", message: "Output IDs must be unique." })
103
+ const count = value.outputs.reduce((sum, output) => sum + (output.kind === "sequence" && value.render !== undefined ? value.render.endFrameExclusive - value.render.startFrame : 1), 0)
104
+ if (count > value.limits.maximumOutputFiles) context.addIssue({ code: "custom", message: "Declared outputs exceed the job's output-file limit." })
105
+ })
106
+ const job = jobShape.transform((value): z.input<typeof jobShape> => ({ ...value, outputs: [...value.outputs].sort((a, b) => studioCompare(a.id, b.id)) })).pipe(jobShape)
107
+ export const StudioJobSchema = studioDocument(job, "studio job")
108
+ export type StudioJob = StudioReadonly<z.infer<typeof StudioJobSchema>>
109
+ export const parseStudioJob = (input: unknown): StudioJob => parseStudioValue(StudioJobSchema, input)
110
+
111
+ export const StudioCapabilityNameSchema = z.enum(["python-authoring", "blend-authoring", "build", "bake", "render", "gpu-render", "image-sequence", "beauty-video", "model-export", "auxiliary-passes", "native-cache", "audio-output"])
112
+ const capability = z.strictObject({ name: StudioCapabilityNameSchema, support: z.enum(["available", "unavailable", "unverified"]), evidence: z.enum(["probe", "qualification"]), receiptSha256: StudioDigestSchema.optional() })
113
+ .refine(value => value.evidence !== "qualification" || value.receiptSha256 !== undefined, "Qualification evidence requires a retained receipt digest.")
114
+ const runtimeShape = z.strictObject({
115
+ kind: z.literal("slopcamera.studio-runtime"), schemaVersion: z.literal(1), engine: StudioEngineSchema,
116
+ tool: z.strictObject({ name: z.string().min(1).max(128), version: z.string().min(1).max(512), executableSha256: StudioDigestSchema }), driverSha256: StudioDigestSchema,
117
+ environment: z.strictObject({ fingerprintSha256: StudioDigestSchema, evidence: z.literal("observed-package-environment"), hermetic: z.literal(false) }),
118
+ capabilities: z.array(capability).max(12),
119
+ }).refine(value => new Set(value.capabilities.map(item => item.name)).size === value.capabilities.length, "Runtime capability names must be unique.")
120
+ const runtime = runtimeShape.transform(value => ({ ...value, capabilities: [...value.capabilities].sort((a, b) => studioCompare(a.name, b.name)) })).pipe(runtimeShape)
121
+ export const StudioRuntimeIdentitySchema = studioDocument(runtime, "studio runtime identity")
122
+ export type StudioRuntimeIdentity = StudioReadonly<z.infer<typeof StudioRuntimeIdentitySchema>>
123
+ export const parseStudioRuntimeIdentity = (input: unknown): StudioRuntimeIdentity => parseStudioValue(StudioRuntimeIdentitySchema, input)
124
+
125
+ export const StudioOutputArtifactSchema = z.strictObject({
126
+ outputId: stableId, path: StudioPathSchema, sha256: StudioDigestSchema, bytes: z.number().int().safe().positive().max(STUDIO_LIMITS.outputBytes), role: StudioOutputRoleSchema, format: StudioOutputFormatSchema,
127
+ frame: z.number().int().nonnegative().max(STUDIO_LIMITS.frameIndexExclusive - 1).optional(),
128
+ })
129
+ export type StudioOutputArtifact = StudioReadonly<z.infer<typeof StudioOutputArtifactSchema>>
@@ -0,0 +1,2 @@
1
+ export * from "./contracts.js"
2
+ export * from "./plan.js"
@@ -0,0 +1,156 @@
1
+ import { z } from "zod"
2
+ import { canonicalJson } from "../code/canonical-json.js"
3
+ import { deepFreezeJson } from "../code/json-snapshot.js"
4
+ import {
5
+ parseStudioJob, parseStudioRuntimeIdentity, parseStudioSourceBundle, StudioCapabilityNameSchema, StudioDigestSchema,
6
+ StudioJobSchema, StudioOutputArtifactSchema, StudioRuntimeIdentitySchema, StudioSourceBundleSchema,
7
+ type StudioJob, type StudioOutputSpec, type StudioRuntimeIdentity, type StudioSourceBundle,
8
+ } from "./contracts.js"
9
+ import { assertDistinctPaths, parseStudioValue, STUDIO_LIMITS, studioCompare, studioDocument, studioHash, studioRequire, type StudioReadonly } from "./shared.js"
10
+
11
+ export const studioSourceBundleSha256 = (input: unknown): string => studioHash("slopcamera.studio-source-bundle/v1", parseStudioSourceBundle(input))
12
+ export const studioJobSha256 = (input: unknown): string => studioHash("slopcamera.studio-job/v1", parseStudioJob(input))
13
+ export const studioRuntimeSha256 = (input: unknown): string => studioHash("slopcamera.studio-runtime/v1", parseStudioRuntimeIdentity(input))
14
+
15
+ /** Resolves only the declared fixed-width frame token. It never evaluates paths or source. */
16
+ export function studioOutputPath(output: StudioOutputSpec, frame?: number): string {
17
+ if (output.kind === "sequence") {
18
+ studioRequire(Number.isInteger(frame) && frame! >= 0 && frame! < STUDIO_LIMITS.frameIndexExclusive, "Sequence output requires an admitted frame index.")
19
+ return output.pathPattern.replace("%06d", String(frame).padStart(6, "0"))
20
+ }
21
+ studioRequire(frame === undefined, "Only sequence outputs have a frame index.")
22
+ return output.path
23
+ }
24
+
25
+ function declaration(job: StudioJob) {
26
+ const files: { readonly id: string; readonly path: string; readonly frame?: number }[] = []
27
+ const directories: { readonly id: string; readonly path: string }[] = []
28
+ for (const output of job.outputs) {
29
+ if (output.kind === "directory") directories.push({ id: output.id, path: output.path })
30
+ else if (output.kind === "file") files.push({ id: output.id, path: output.path })
31
+ else {
32
+ studioRequire(job.render !== undefined, "Sequence requires a render interval.")
33
+ for (let frame = job.render.startFrame; frame < job.render.endFrameExclusive; frame++) files.push({ id: output.id, path: studioOutputPath(output, frame), frame })
34
+ }
35
+ }
36
+ studioRequire(files.length + directories.length <= job.limits.maximumOutputFiles, "Declared outputs exceed the output-file budget.")
37
+ assertDistinctPaths([...files, ...directories].map(item => item.path))
38
+ return { files, directories }
39
+ }
40
+ function requiredCapabilities(bundle: StudioSourceBundle, job: StudioJob): z.infer<typeof StudioCapabilityNameSchema>[] {
41
+ const values = new Set<z.infer<typeof StudioCapabilityNameSchema>>([bundle.entrypoint.kind === "blend" ? "blend-authoring" : "python-authoring", job.stage])
42
+ if (job.engine.engine === "blender" && job.engine.device === "gpu" && job.stage === "render") values.add("gpu-render")
43
+ for (const output of job.outputs) {
44
+ if (output.kind === "sequence") values.add("image-sequence")
45
+ if (output.role === "beauty" && ["mov", "mp4", "webm"].includes(output.format)) values.add("beauty-video")
46
+ if (output.role === "model") values.add("model-export")
47
+ if (output.role === "auxiliary") values.add("auxiliary-passes")
48
+ if (output.role === "simulation-cache") values.add("native-cache")
49
+ if (output.role === "audio") values.add("audio-output")
50
+ }
51
+ return [...values].sort(studioCompare)
52
+ }
53
+ function derivePlan(input: { readonly bundle: StudioSourceBundle; readonly job: StudioJob; readonly runtime?: StudioRuntimeIdentity | undefined }) {
54
+ const { bundle, job, runtime } = input
55
+ const bundleSha256 = studioSourceBundleSha256(bundle), jobSha256 = studioJobSha256(job)
56
+ studioRequire(bundleSha256 === job.bundleSha256, "Job does not bind the exact source bundle.")
57
+ studioRequire(bundle.engine === job.engine.engine && (runtime === undefined || runtime.engine === bundle.engine), "Bundle, job, and runtime must use one engine.")
58
+ studioRequire(bundle.entrypoint.kind !== "blend" || job.stage === "render", "Blend entrypoints support render stage only; explicit Python authoring owns builds and bakes.")
59
+ const declared = declaration(job), required = requiredCapabilities(bundle, job)
60
+ const capabilityChecks = required.map(name => ({ name, support: runtime === undefined ? "unbound" as const : runtime.capabilities.find(item => item.name === name)?.support ?? "unverified" as const }))
61
+ const readiness = runtime === undefined ? "runtime-unbound" as const
62
+ : capabilityChecks.some(item => item.support === "unavailable") ? "capability-unavailable" as const
63
+ : capabilityChecks.some(item => item.support === "unverified") ? "capability-unverified" as const : "authorization-required" as const
64
+ const body = {
65
+ kind: "slopcamera.studio-plan" as const, schemaVersion: 1 as const, bundle, job, ...(runtime === undefined ? {} : { runtime, runtimeSha256: studioRuntimeSha256(runtime) }),
66
+ bundleSha256, jobSha256, sourceBytes: bundle.files.reduce((total, file) => total + file.bytes, 0),
67
+ frameCount: job.render === undefined ? 0 : job.render.endFrameExclusive - job.render.startFrame,
68
+ outputCount: { minimum: declared.files.length + declared.directories.length, maximum: declared.directories.length === 0 ? declared.files.length : job.limits.maximumOutputFiles },
69
+ requiredCapabilities: required, capabilityChecks, readiness,
70
+ }
71
+ return { ...body, planSha256: studioHash("slopcamera.studio-plan/v1", body) }
72
+ }
73
+ const plan = z.strictObject({
74
+ kind: z.literal("slopcamera.studio-plan"), schemaVersion: z.literal(1), bundle: StudioSourceBundleSchema, job: StudioJobSchema,
75
+ runtime: StudioRuntimeIdentitySchema.optional(), runtimeSha256: StudioDigestSchema.optional(), bundleSha256: StudioDigestSchema, jobSha256: StudioDigestSchema, planSha256: StudioDigestSchema,
76
+ sourceBytes: z.number().int().safe().nonnegative().max(STUDIO_LIMITS.sourceBytes), frameCount: z.number().int().nonnegative().max(STUDIO_LIMITS.frames),
77
+ outputCount: z.strictObject({ minimum: z.number().int().positive().max(STUDIO_LIMITS.outputFiles), maximum: z.number().int().positive().max(STUDIO_LIMITS.outputFiles) }),
78
+ requiredCapabilities: z.array(StudioCapabilityNameSchema).min(1).max(12),
79
+ capabilityChecks: z.array(z.strictObject({ name: StudioCapabilityNameSchema, support: z.enum(["available", "unavailable", "unverified", "unbound"]) })).min(1).max(12),
80
+ readiness: z.enum(["runtime-unbound", "capability-unavailable", "capability-unverified", "authorization-required"]),
81
+ }).superRefine((value, context) => {
82
+ try {
83
+ const derived = derivePlan({ bundle: value.bundle, job: value.job, ...(value.runtime === undefined ? {} : { runtime: value.runtime }) })
84
+ if (canonicalJson(derived) !== canonicalJson(value)) context.addIssue({ code: "custom", message: "Studio plan differs from its canonical source, job, runtime, or admission derivation." })
85
+ } catch (error) { context.addIssue({ code: "custom", message: error instanceof Error ? error.message : "Invalid studio plan." }) }
86
+ })
87
+ export const StudioPlanSchema = studioDocument(plan, "studio plan")
88
+ export type StudioPlan = StudioReadonly<z.infer<typeof StudioPlanSchema>>
89
+ export const parseStudioPlan = (input: unknown): StudioPlan => parseStudioValue(StudioPlanSchema, input)
90
+ export function planStudioJob(input: { readonly bundle: unknown; readonly job: unknown; readonly runtime?: unknown }): StudioPlan {
91
+ const captured = parseStudioValue(studioDocument(z.strictObject({ bundle: StudioSourceBundleSchema, job: StudioJobSchema, runtime: StudioRuntimeIdentitySchema.optional() }), "studio planning input"), input)
92
+ return parseStudioPlan(derivePlan(captured))
93
+ }
94
+
95
+ const failure = z.strictObject({ code: z.enum(["subprocess", "cancelled", "deadline", "validation", "custody", "publication", "unavailable"]), message: z.string().min(1).max(2048) })
96
+ const receiptCommon = {
97
+ kind: z.literal("slopcamera.studio-receipt"), schemaVersion: z.literal(1), jobId: z.string().regex(/^studio_[a-zA-Z0-9][a-zA-Z0-9_-]{0,120}$/u), attemptId: z.string().regex(/^attempt_[a-zA-Z0-9][a-zA-Z0-9_-]{0,120}$/u),
98
+ planSha256: StudioDigestSchema, bundleSha256: StudioDigestSchema, jobSha256: StudioDigestSchema, runtime: StudioRuntimeIdentitySchema, runtimeSha256: StudioDigestSchema,
99
+ startedAt: z.iso.datetime({ offset: true }), finishedAt: z.iso.datetime({ offset: true }), outputs: z.array(StudioOutputArtifactSchema).max(STUDIO_LIMITS.outputFiles),
100
+ }
101
+ const receiptShape = z.discriminatedUnion("state", [
102
+ z.strictObject({ ...receiptCommon, state: z.literal("succeeded"), custody: z.literal("closed"), exitCode: z.literal(0) }),
103
+ z.strictObject({ ...receiptCommon, state: z.literal("failed"), custody: z.literal("closed"), exitCode: z.number().int().min(-255).max(255).nullable(), failure }),
104
+ z.strictObject({ ...receiptCommon, state: z.literal("unknown-custody"), custody: z.literal("unknown"), exitCode: z.number().int().min(-255).max(255).nullable(), failure }),
105
+ ]).superRefine((value, context) => {
106
+ if (studioRuntimeSha256(value.runtime) !== value.runtimeSha256) context.addIssue({ code: "custom", message: "Receipt runtime digest differs from its evidence." })
107
+ if (Date.parse(value.finishedAt) < Date.parse(value.startedAt)) context.addIssue({ code: "custom", message: "Receipt finishes before it starts." })
108
+ try { assertDistinctPaths(value.outputs.map(item => item.path)) } catch { context.addIssue({ code: "custom", message: "Receipt output paths collide." }) }
109
+ if (value.outputs.reduce((sum, item) => sum + item.bytes, 0) > STUDIO_LIMITS.outputBytes) context.addIssue({ code: "custom", message: "Receipt exceeds the global output byte bound." })
110
+ })
111
+ const receipt = receiptShape.transform((value): z.input<typeof receiptShape> => ({ ...value, outputs: [...value.outputs].sort((a, b) => studioCompare(a.path, b.path)) })).pipe(receiptShape)
112
+ export const StudioReceiptSchema = studioDocument(receipt, "studio receipt")
113
+ export type StudioReceipt = StudioReadonly<z.infer<typeof StudioReceiptSchema>>
114
+ export const parseStudioReceipt = (input: unknown): StudioReceipt => parseStudioValue(StudioReceiptSchema, input)
115
+
116
+ /** Validates retained declarations and provenance; host code must verify actual files and native custody. */
117
+ export function validateStudioReceipt(input: { readonly plan: unknown; readonly receipt: unknown }): StudioReceipt {
118
+ const captured = parseStudioValue(studioDocument(z.strictObject({ plan: StudioPlanSchema, receipt: StudioReceiptSchema }), "studio receipt validation input"), input)
119
+ const { plan, receipt } = captured
120
+ studioRequire(receipt.jobId === plan.job.jobId && receipt.planSha256 === plan.planSha256 && receipt.jobSha256 === plan.jobSha256 && receipt.bundleSha256 === plan.bundleSha256, "Receipt identity differs from the exact planned job.")
121
+ studioRequire(plan.runtime !== undefined && receipt.runtimeSha256 === plan.runtimeSha256 && canonicalJson(receipt.runtime) === canonicalJson(plan.runtime), "Execution receipt requires the exact planned runtime binding.")
122
+ if (receipt.state === "succeeded") studioRequire(plan.readiness === "authorization-required", "A successful receipt requires available observed runtime capabilities; consent remains host-owned.")
123
+ studioRequire(receipt.outputs.length <= plan.job.limits.maximumOutputFiles && receipt.outputs.reduce((total, output) => total + output.bytes, 0) <= plan.job.limits.maximumOutputBytes, "Receipt exceeds the planned output budget.")
124
+ const declared = declaration(plan.job), expected = new Map(declared.files.map(file => [file.path, file]))
125
+ const counts = new Map<string, number>()
126
+ for (const artifact of receipt.outputs) {
127
+ const specification = plan.job.outputs.find(output => output.id === artifact.outputId)
128
+ studioRequire(specification !== undefined, "Receipt contains an undeclared output ID.")
129
+ studioRequire(artifact.role === specification.role && artifact.format === specification.format, "Receipt output role or format differs from its declaration.")
130
+ if (specification.kind === "directory") {
131
+ studioRequire(artifact.path.startsWith(`${specification.path}/`) && artifact.frame === undefined, "Cache artifact must be a file inside its declared directory.")
132
+ } else {
133
+ const wanted = expected.get(artifact.path)
134
+ studioRequire(wanted !== undefined && wanted.id === artifact.outputId && artifact.frame === wanted.frame, "Receipt contains an undeclared file or incorrect frame index.")
135
+ expected.delete(artifact.path)
136
+ }
137
+ counts.set(artifact.outputId, (counts.get(artifact.outputId) ?? 0) + 1)
138
+ }
139
+ if (receipt.state === "succeeded") {
140
+ studioRequire(expected.size === 0 && declared.directories.every(directory => (counts.get(directory.id) ?? 0) > 0), "Successful receipt must cover every declared file, frame, and cache directory.")
141
+ }
142
+ return deepFreezeJson(receipt)
143
+ }
144
+
145
+ export function inspectStudioBundle(input: unknown) {
146
+ const bundle = parseStudioSourceBundle(input)
147
+ return deepFreezeJson({ engine: bundle.engine, entrypoint: bundle.entrypoint, bundleSha256: studioSourceBundleSha256(bundle), files: bundle.files.length,
148
+ sourceBytes: bundle.files.reduce((total, file) => total + file.bytes, 0), executed: false as const, dependencyDiscovery: "explicit-files-only" as const })
149
+ }
150
+ export function inspectStudioPlan(input: unknown) {
151
+ const plan = parseStudioPlan(input)
152
+ return deepFreezeJson({ jobId: plan.job.jobId, stage: plan.job.stage, engine: plan.job.engine.engine, planSha256: plan.planSha256,
153
+ sourceBytes: plan.sourceBytes, frameCount: plan.frameCount, outputCount: plan.outputCount, readiness: plan.readiness,
154
+ capabilityChecks: plan.capabilityChecks, execution: plan.job.execution, limits: plan.job.limits, executed: false as const,
155
+ outputs: plan.job.outputs.map(output => ({ id: output.id, role: output.role, format: output.format, kind: output.kind, path: output.kind === "sequence" ? output.pathPattern : output.path })) })
156
+ }
@@ -0,0 +1,41 @@
1
+ import { z } from "zod"
2
+ import { boundedCanonicalJsonSha256, compareUtf16Strings } from "../code/canonical-json.js"
3
+ import { SlopcameraCodeError } from "../code/errors.js"
4
+ import { createBoundedJsonValueSnapshot, deepFreezeJson } from "../code/json-snapshot.js"
5
+
6
+ export const STUDIO_LIMITS = Object.freeze({
7
+ documentBytes: 32 * 1024 * 1024, documentDepth: 32, documentValues: 500_000,
8
+ sourceFiles: 512, sourceBytes: 4 * 1024 ** 3, outputSpecifications: 32,
9
+ outputFiles: 25_000, outputBytes: 64 * 1024 ** 3, timeoutSeconds: 6 * 60 * 60,
10
+ frames: 25_000, frameIndexExclusive: 1_000_000, dimension: 8192, pixels: 33_554_432,
11
+ parameterBytes: 256 * 1024, parameterDepth: 16, parameterValues: 20_000,
12
+ })
13
+ export type StudioReadonly<T> = T extends object ? { readonly [Key in keyof T]: StudioReadonly<T[Key]> } : T
14
+ export function studioDocument<Schema extends z.ZodType>(schema: Schema, name: string) {
15
+ return z.preprocess(value => value === undefined ? undefined : createBoundedJsonValueSnapshot(value, STUDIO_LIMITS.documentBytes, name,
16
+ { maximumDepth: STUDIO_LIMITS.documentDepth, maximumValues: STUDIO_LIMITS.documentValues }).value, schema)
17
+ }
18
+ export function parseStudioValue<Schema extends z.ZodType>(schema: Schema, input: unknown): StudioReadonly<z.infer<Schema>> {
19
+ try { return deepFreezeJson(schema.parse(input)) as StudioReadonly<z.infer<Schema>> }
20
+ catch (error) {
21
+ if (error instanceof SlopcameraCodeError) throw error
22
+ throw new SlopcameraCodeError("invalid-data", error instanceof z.ZodError ? error.issues[0]?.message ?? "Invalid studio document." : "Invalid studio document.")
23
+ }
24
+ }
25
+ export function studioHash(domain: string, value: unknown): string {
26
+ return boundedCanonicalJsonSha256({ domain, value }, { maximumBytes: STUDIO_LIMITS.documentBytes, maximumDepth: STUDIO_LIMITS.documentDepth + 2, maximumValues: STUDIO_LIMITS.documentValues + 4 })
27
+ }
28
+ export const studioCompare = compareUtf16Strings
29
+ export function studioRequire(condition: unknown, message: string): asserts condition {
30
+ if (!condition) throw new SlopcameraCodeError("invalid-data", message)
31
+ }
32
+ export function pathKey(path: string): string { return path.toLowerCase() }
33
+ export function assertDistinctPaths(paths: readonly string[]): void {
34
+ const names = new Set(paths.map(pathKey))
35
+ studioRequire(names.size === paths.length, "Studio paths collide.")
36
+ for (const name of names) {
37
+ for (let offset = name.indexOf("/"); offset !== -1; offset = name.indexOf("/", offset + 1)) {
38
+ studioRequire(!names.has(name.slice(0, offset)), "Studio paths use a file as an ancestor.")
39
+ }
40
+ }
41
+ }
package/src/theme.ts ADDED
@@ -0,0 +1,68 @@
1
+ import type { ColorMode, DiagramConfig, PartialTheme, ThemeColors, Tone } from "./types.js"
2
+
3
+ const tones: readonly Tone[] = [
4
+ "neutral",
5
+ "blue",
6
+ "orange",
7
+ "green",
8
+ "red",
9
+ "purple",
10
+ "yellow",
11
+ ]
12
+
13
+ const defaults: Readonly<Record<ColorMode, ThemeColors>> = {
14
+ light: {
15
+ background: "#ffffff",
16
+ foreground: "#18181b",
17
+ muted: "#71717a",
18
+ stroke: "#27272a",
19
+ tones: {
20
+ neutral: { fill: "#f4f4f5", stroke: "#52525b", text: "#18181b" },
21
+ blue: { fill: "#dbeafe", stroke: "#2563eb", text: "#1e3a8a" },
22
+ orange: { fill: "#ffedd5", stroke: "#ea580c", text: "#7c2d12" },
23
+ green: { fill: "#dcfce7", stroke: "#16a34a", text: "#14532d" },
24
+ red: { fill: "#fee2e2", stroke: "#dc2626", text: "#7f1d1d" },
25
+ purple: { fill: "#f3e8ff", stroke: "#9333ea", text: "#581c87" },
26
+ yellow: { fill: "#fef9c3", stroke: "#ca8a04", text: "#713f12" },
27
+ },
28
+ },
29
+ dark: {
30
+ background: "#09090b",
31
+ foreground: "#fafafa",
32
+ muted: "#a1a1aa",
33
+ stroke: "#d4d4d8",
34
+ tones: {
35
+ neutral: { fill: "#27272a", stroke: "#a1a1aa", text: "#fafafa" },
36
+ blue: { fill: "#172554", stroke: "#60a5fa", text: "#dbeafe" },
37
+ orange: { fill: "#431407", stroke: "#fb923c", text: "#ffedd5" },
38
+ green: { fill: "#052e16", stroke: "#4ade80", text: "#dcfce7" },
39
+ red: { fill: "#450a0a", stroke: "#f87171", text: "#fee2e2" },
40
+ purple: { fill: "#3b0764", stroke: "#c084fc", text: "#f3e8ff" },
41
+ yellow: { fill: "#422006", stroke: "#facc15", text: "#fef9c3" },
42
+ },
43
+ },
44
+ }
45
+
46
+ function mergeTheme(base: ThemeColors, override: PartialTheme | undefined): ThemeColors {
47
+ if (override === undefined) return base
48
+ return {
49
+ background: override.background ?? base.background,
50
+ foreground: override.foreground ?? base.foreground,
51
+ muted: override.muted ?? base.muted,
52
+ stroke: override.stroke ?? base.stroke,
53
+ tones: Object.fromEntries(
54
+ tones.map((tone) => [
55
+ tone,
56
+ {
57
+ fill: override.tones?.[tone]?.fill ?? base.tones[tone].fill,
58
+ stroke: override.tones?.[tone]?.stroke ?? base.tones[tone].stroke,
59
+ text: override.tones?.[tone]?.text ?? base.tones[tone].text,
60
+ },
61
+ ]),
62
+ ) as ThemeColors["tones"],
63
+ }
64
+ }
65
+
66
+ export function resolveTheme(mode: ColorMode, config: DiagramConfig): ThemeColors {
67
+ return mergeTheme(defaults[mode], config.theme?.[mode])
68
+ }