@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,552 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { link, open, rm } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import {
5
+ AnalysisIdSchema,
6
+ AnalysisSubjectSchema,
7
+ AnalysisToolSchema,
8
+ ProjectAnalysisReferenceSchema,
9
+ ProjectInactivityAnalysisV1Schema,
10
+ ProjectInactivityConfigSchema,
11
+ VideoProjectV1Schema,
12
+ type AnalysisSubject,
13
+ type ProjectAssetV1,
14
+ type ProjectInactivityAnalysisV1,
15
+ type ProjectPlacementV1,
16
+ type RecordingEventV1,
17
+ type RecordingManifestV1,
18
+ type VideoProjectV1,
19
+ } from "../contracts";
20
+ import {
21
+ DEFAULT_MINIMUM_FREEZE_CONFIDENCE,
22
+ canonicalSlopcameraPersistenceDocument,
23
+ canonicalJson,
24
+ canonicalJsonSha256,
25
+ hashPlacementSync,
26
+ hashProjectStructure,
27
+ mapAssetIntervalToProjectSlices,
28
+ planMappedInactivityCuts,
29
+ recordingInteractionIntervals,
30
+ saveAnalysisArtifact,
31
+ saveVideoProject,
32
+ sha256Hex,
33
+ unionIntervals,
34
+ } from "../core";
35
+ import { FfmpegInactivityAnalyzer } from "./analyzer";
36
+ import { mapBounded } from "./bounded-map";
37
+ import { loadRecordingEvents, openRecording } from "./bundle-service";
38
+ import { CliError } from "./errors";
39
+ import type { ProcessRunner } from "./io";
40
+ import { ensurePhysicalPrivateDirectoryWithin } from "./paths";
41
+ import { resolveVerifiedProjectMedia } from "./project-media-integrity";
42
+ import { projectAnalysisPath, type OpenProject } from "./project-service";
43
+
44
+ type VideoStream = Extract<ProjectAssetV1["streams"][number], { readonly kind: "video" }>;
45
+ type AudioStream = Extract<ProjectAssetV1["streams"][number], { readonly kind: "audio" }>;
46
+ export type InactivityReference = Extract<
47
+ VideoProjectV1["analyses"][number],
48
+ { readonly kind: "inactivity" }
49
+ >;
50
+
51
+ interface SelectedStream<Stream extends VideoStream | AudioStream> {
52
+ readonly asset: ProjectAssetV1;
53
+ readonly placement: ProjectPlacementV1;
54
+ readonly stream: Stream;
55
+ readonly subject: AnalysisSubject;
56
+ }
57
+
58
+ export type ProjectInactivityConfig = ProjectInactivityAnalysisV1["config"];
59
+
60
+ export const DEFAULT_PROJECT_INACTIVITY_CONFIG = {
61
+ cursorMovementThresholdPx: 2,
62
+ edgeHandleUs: 250_000,
63
+ interactionHandleUs: 750_000,
64
+ minimumCutUs: 3_000_000,
65
+ minimumFreezeConfidence: DEFAULT_MINIMUM_FREEZE_CONFIDENCE,
66
+ motionThreshold: 0.003,
67
+ requireAudioSilence: true,
68
+ } as const satisfies ProjectInactivityConfig;
69
+
70
+ function streamSubject(asset: ProjectAssetV1, stream: VideoStream | AudioStream): AnalysisSubject {
71
+ return AnalysisSubjectSchema.parse({
72
+ assetId: asset.assetId,
73
+ integritySha256: canonicalJsonSha256({ assetDurationUs: asset.durationUs, stream }),
74
+ streamId: stream.streamId,
75
+ });
76
+ }
77
+
78
+ function selectedStreams<Stream extends VideoStream | AudioStream>(
79
+ project: VideoProjectV1,
80
+ kind: Stream["kind"],
81
+ ): readonly SelectedStream<Stream>[] {
82
+ const selected: SelectedStream<Stream>[] = [];
83
+ for (const placement of project.placements) {
84
+ if (!placement.enabled) continue;
85
+ const asset = project.assets.find(candidate => candidate.assetId === placement.assetId)!;
86
+ const presentations = kind === "video" ? placement.video : placement.audio;
87
+ for (const configured of presentations) {
88
+ if (!configured.presentation.enabled) continue;
89
+ const stream = asset.streams.find(candidate => candidate.streamId === configured.streamId);
90
+ if (stream?.kind !== kind) continue;
91
+ if (kind === "video" && stream.role !== "screen") continue;
92
+ selected.push({
93
+ asset,
94
+ placement,
95
+ stream: stream as Stream,
96
+ subject: streamSubject(asset, stream),
97
+ });
98
+ }
99
+ }
100
+ return selected.sort((left, right) => (
101
+ left.placement.placementId.localeCompare(right.placement.placementId)
102
+ || left.stream.streamId.localeCompare(right.stream.streamId)
103
+ ));
104
+ }
105
+
106
+ async function detectAssetIntervals(
107
+ selected: SelectedStream<VideoStream | AudioStream>,
108
+ analyzer: FfmpegInactivityAnalyzer,
109
+ repositoryRoot: string,
110
+ config: ProjectInactivityConfig,
111
+ ): Promise<readonly { readonly endUs: number; readonly startUs: number }[]> {
112
+ const detected: Array<readonly { readonly endUs: number; readonly startUs: number }[]> = [];
113
+ for (const segment of selected.stream.segments) {
114
+ const sourceOffsetUs = segment.assetRange.startUs - segment.fileRange.startUs;
115
+ const path = await resolveVerifiedProjectMedia({
116
+ expected: { bytes: segment.bytes, sha256: segment.sha256 },
117
+ label: `Inactivity segment ${segment.path}:${segment.streamIndex}`,
118
+ path: segment.path,
119
+ repositoryRoot,
120
+ });
121
+ const intervals = selected.stream.kind === "video"
122
+ ? await analyzer.freeze(
123
+ path,
124
+ segment.streamIndex,
125
+ config.minimumCutUs,
126
+ config.motionThreshold,
127
+ sourceOffsetUs,
128
+ )
129
+ : await analyzer.silence(
130
+ path,
131
+ segment.streamIndex,
132
+ config.minimumCutUs,
133
+ sourceOffsetUs,
134
+ );
135
+ detected.push(intervals.flatMap(interval => {
136
+ const startUs = Math.max(interval.startUs, segment.assetRange.startUs, selected.placement.assetRange.startUs);
137
+ const endUs = Math.min(interval.endUs, segment.assetRange.endUs, selected.placement.assetRange.endUs);
138
+ return endUs > startUs ? [{ endUs, startUs }] : [];
139
+ }));
140
+ }
141
+ return unionIntervals(detected.flat());
142
+ }
143
+
144
+ async function displayEvidence(
145
+ selected: SelectedStream<VideoStream>,
146
+ analyzer: FfmpegInactivityAnalyzer,
147
+ repositoryRoot: string,
148
+ config: ProjectInactivityConfig,
149
+ ): Promise<ProjectInactivityAnalysisV1["displays"][number]> {
150
+ const intervals = await detectAssetIntervals(selected, analyzer, repositoryRoot, config);
151
+ return {
152
+ intervals: intervals.flatMap(assetRange => (
153
+ mapAssetIntervalToProjectSlices(selected.placement, assetRange).map(slice => ({
154
+ assetRange: slice.asset,
155
+ confidence: 1,
156
+ meanFrameDifference: config.motionThreshold,
157
+ projectRange: slice.project,
158
+ }))
159
+ )),
160
+ placementAssetRange: selected.placement.assetRange,
161
+ placementId: selected.placement.placementId,
162
+ subject: selected.subject,
163
+ syncMapSha256: hashPlacementSync(selected.placement),
164
+ };
165
+ }
166
+
167
+ async function audioEvidence(
168
+ selected: SelectedStream<AudioStream>,
169
+ analyzer: FfmpegInactivityAnalyzer,
170
+ repositoryRoot: string,
171
+ config: ProjectInactivityConfig,
172
+ ): Promise<ProjectInactivityAnalysisV1["audio"][number]> {
173
+ const intervals = await detectAssetIntervals(selected, analyzer, repositoryRoot, config);
174
+ return {
175
+ intervals: intervals.flatMap(assetRange => (
176
+ mapAssetIntervalToProjectSlices(selected.placement, assetRange).map(slice => ({
177
+ assetRange: slice.asset,
178
+ peakDb: -45,
179
+ projectRange: slice.project,
180
+ }))
181
+ )),
182
+ placementAssetRange: selected.placement.assetRange,
183
+ placementId: selected.placement.placementId,
184
+ subject: selected.subject,
185
+ syncMapSha256: hashPlacementSync(selected.placement),
186
+ };
187
+ }
188
+
189
+ export interface ProjectInactivityReferenceEvidence
190
+ extends Pick<
191
+ ProjectInactivityAnalysisV1,
192
+ "interactions" | "referenceRecording"
193
+ > {
194
+ readonly events: readonly RecordingEventV1[];
195
+ readonly input: unknown;
196
+ readonly manifest: RecordingManifestV1 | null;
197
+ }
198
+
199
+ export async function loadProjectInactivityReferenceEvidence(
200
+ project: VideoProjectV1,
201
+ artifactRoot: string,
202
+ cursorMovementThresholdPx: number,
203
+ ): Promise<ProjectInactivityReferenceEvidence> {
204
+ const placement = project.placements.find(candidate => candidate.placementId === project.referencePlacementId)!;
205
+ const asset = project.assets.find(candidate => candidate.assetId === placement.assetId)!;
206
+ if (asset.source.kind !== "recording") {
207
+ return {
208
+ events: [],
209
+ input: null,
210
+ interactions: [],
211
+ manifest: null,
212
+ referenceRecording: null,
213
+ };
214
+ }
215
+ const recording = await openRecording(artifactRoot, asset.source.recordingId);
216
+ const events = await loadRecordingEvents(recording);
217
+ const interactions = recordingInteractionIntervals(
218
+ events,
219
+ cursorMovementThresholdPx,
220
+ ).flatMap(interaction => (
221
+ mapAssetIntervalToProjectSlices(placement, interaction.range).map(slice => ({
222
+ assetRange: slice.asset,
223
+ projectRange: slice.project,
224
+ source: interaction.source,
225
+ }))
226
+ )).sort((left, right) => (
227
+ left.projectRange.startUs - right.projectRange.startUs
228
+ || left.projectRange.endUs - right.projectRange.endUs
229
+ || left.source.localeCompare(right.source)
230
+ ));
231
+ return {
232
+ events,
233
+ input: recording.manifest.eventStreams.map(stream => ({
234
+ endUs: stream.endUs,
235
+ integrity: stream.integrity,
236
+ path: stream.path,
237
+ recordCount: stream.recordCount,
238
+ startUs: stream.startUs,
239
+ })),
240
+ interactions,
241
+ manifest: recording.manifest,
242
+ referenceRecording: {
243
+ assetId: asset.assetId,
244
+ placementId: placement.placementId,
245
+ recordingId: asset.source.recordingId,
246
+ syncMapSha256: hashPlacementSync(placement),
247
+ },
248
+ };
249
+ }
250
+
251
+ function isMissingFile(error: unknown): boolean {
252
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
253
+ }
254
+
255
+ async function requireUnusedAnalysisPath(project: OpenProject, path: string): Promise<void> {
256
+ try {
257
+ await project.fileSystem.readText(path);
258
+ } catch (error) {
259
+ if (isMissingFile(error)) return;
260
+ throw error;
261
+ }
262
+ throw new CliError("conflict", `Analysis artifact already exists and will not be overwritten: ${path}`);
263
+ }
264
+
265
+ export interface AnalyzeProjectInactivityOptions {
266
+ readonly analysisId?: string;
267
+ readonly artifactRoot: string;
268
+ readonly config?: ProjectInactivityConfig;
269
+ readonly ffmpeg: string;
270
+ readonly ffprobe: string;
271
+ readonly ffmpegVersion: string;
272
+ readonly now: Date;
273
+ readonly project: OpenProject;
274
+ /**
275
+ * A host-opened, integrity-verified metadata snapshot. Workflow execution
276
+ * supplies this so exact-input verification and analysis consume the same
277
+ * event bytes instead of reopening a mutable recording path.
278
+ */
279
+ readonly referenceEvidence?: ProjectInactivityReferenceEvidence;
280
+ readonly repositoryRoot: string;
281
+ readonly runner: ProcessRunner;
282
+ readonly toolVersion: string;
283
+ }
284
+
285
+ export interface PersistedProjectInactivityAnalysis {
286
+ readonly analysis: ProjectInactivityAnalysisV1;
287
+ readonly analysisPath: string;
288
+ readonly project: VideoProjectV1;
289
+ readonly reference: InactivityReference;
290
+ }
291
+
292
+ export interface AnalyzedProjectInactivity {
293
+ readonly analysis: ProjectInactivityAnalysisV1;
294
+ }
295
+
296
+ /** Analyze all enabled screen/audio presentations without publishing project state. */
297
+ export async function analyzeProjectInactivity(
298
+ options: AnalyzeProjectInactivityOptions,
299
+ ): Promise<AnalyzedProjectInactivity> {
300
+ const config = ProjectInactivityConfigSchema.parse(
301
+ options.config ?? DEFAULT_PROJECT_INACTIVITY_CONFIG,
302
+ );
303
+ const analysisId = AnalysisIdSchema.parse(
304
+ options.analysisId ?? `analysis_${randomUUID().replaceAll("-", "")}`,
305
+ );
306
+ if (options.project.project.analyses.some(existing => existing.analysisId === analysisId)) {
307
+ throw new CliError("conflict", `Project analysis already exists: ${analysisId}`);
308
+ }
309
+ if (options.now.getTime() < Date.parse(options.project.project.updatedAt)) {
310
+ throw new CliError("conflict", "The inactivity analysis time cannot precede the project update time.");
311
+ }
312
+ const analysisPath = projectAnalysisPath("inactivity", analysisId);
313
+ await requireUnusedAnalysisPath(options.project, analysisPath);
314
+
315
+ const screens = selectedStreams<VideoStream>(options.project.project, "video");
316
+ if (screens.length === 0) {
317
+ throw new CliError("invalid-data", "Project has no enabled screen stream to analyze.");
318
+ }
319
+ const audioStreams = config.requireAudioSilence
320
+ ? selectedStreams<AudioStream>(options.project.project, "audio")
321
+ : [];
322
+ const analyzer = new FfmpegInactivityAnalyzer({
323
+ ffmpeg: options.ffmpeg,
324
+ ffprobe: options.ffprobe,
325
+ runner: options.runner,
326
+ });
327
+ const tasks: Array<
328
+ | { readonly kind: "audio"; readonly selected: SelectedStream<AudioStream> }
329
+ | { readonly kind: "display"; readonly selected: SelectedStream<VideoStream> }
330
+ > = [
331
+ ...screens.map(selected => ({ kind: "display" as const, selected })),
332
+ ...audioStreams.map(selected => ({ kind: "audio" as const, selected })),
333
+ ];
334
+ const [analyzed, reference] = await Promise.all([
335
+ mapBounded(tasks, 4, async task => task.kind === "display"
336
+ ? {
337
+ evidence: await displayEvidence(task.selected, analyzer, options.repositoryRoot, config),
338
+ kind: task.kind,
339
+ } as const
340
+ : {
341
+ evidence: await audioEvidence(task.selected, analyzer, options.repositoryRoot, config),
342
+ kind: task.kind,
343
+ } as const),
344
+ options.referenceEvidence === undefined
345
+ ? loadProjectInactivityReferenceEvidence(
346
+ options.project.project,
347
+ options.artifactRoot,
348
+ config.cursorMovementThresholdPx,
349
+ )
350
+ : Promise.resolve(options.referenceEvidence),
351
+ ]);
352
+ const displays = analyzed.flatMap(item => item.kind === "display" ? [item.evidence] : []);
353
+ const audio = analyzed.flatMap(item => item.kind === "audio" ? [item.evidence] : []);
354
+ const plan = planMappedInactivityCuts({
355
+ audioIntervals: audio.map(item => item.intervals.map(interval => interval.projectRange)),
356
+ displayIntervals: displays.map(item => item.intervals
357
+ .filter(interval => interval.confidence >= config.minimumFreezeConfidence)
358
+ .map(interval => interval.projectRange)),
359
+ interactionIntervals: reference.interactions.map(interaction => interaction.projectRange),
360
+ sourceDurationUs: options.project.project.timeline.durationUs,
361
+ }, {
362
+ cursorMovementThresholdPx: config.cursorMovementThresholdPx,
363
+ edgeHandleUs: config.edgeHandleUs,
364
+ interactionHandleUs: config.interactionHandleUs,
365
+ minimumCutUs: config.minimumCutUs,
366
+ minimumFreezeConfidence: config.minimumFreezeConfidence,
367
+ requireAudioSilence: config.requireAudioSilence,
368
+ });
369
+ const projectStructureSha256 = hashProjectStructure(options.project.project);
370
+ const inputDigest = canonicalJsonSha256({
371
+ audio: audio.map(item => ({
372
+ placementId: item.placementId,
373
+ subject: item.subject,
374
+ syncMapSha256: item.syncMapSha256,
375
+ })),
376
+ config,
377
+ displays: displays.map(item => ({
378
+ placementId: item.placementId,
379
+ subject: item.subject,
380
+ syncMapSha256: item.syncMapSha256,
381
+ })),
382
+ projectStructureSha256,
383
+ referenceEvents: reference.input,
384
+ });
385
+ const createdAt = options.now.toISOString();
386
+ const analysis = ProjectInactivityAnalysisV1Schema.parse({
387
+ analysisId,
388
+ audio,
389
+ config,
390
+ createdAt,
391
+ displays,
392
+ durationUs: options.project.project.timeline.durationUs,
393
+ inputDigest,
394
+ interactions: reference.interactions,
395
+ kind: "slopcamera.project-inactivity-analysis",
396
+ projectId: options.project.project.projectId,
397
+ projectStructureSha256,
398
+ referenceRecording: reference.referenceRecording,
399
+ result: {
400
+ candidateCount: plan.candidateCount,
401
+ protectedInteractionCount: plan.protectedInteractionCount,
402
+ recommendedRanges: plan.cuts,
403
+ },
404
+ schemaVersion: 1,
405
+ tool: AnalysisToolSchema.parse({
406
+ name: "ffmpeg-freezedetect-silencedetect",
407
+ profile: "all-enabled-project-streams-v1",
408
+ version: `${options.toolVersion}; ${options.ffmpegVersion}`.slice(0, 128),
409
+ }),
410
+ });
411
+ return { analysis };
412
+ }
413
+
414
+ export function buildProjectInactivityAnalysisReference(
415
+ analysisInput: ProjectInactivityAnalysisV1,
416
+ analysisPath: string,
417
+ ): InactivityReference {
418
+ const analysis = ProjectInactivityAnalysisV1Schema.parse(analysisInput);
419
+ const reference = ProjectAnalysisReferenceSchema.parse({
420
+ analysisId: analysis.analysisId,
421
+ audioStreams: analysis.audio.length,
422
+ createdAt: analysis.createdAt,
423
+ displayStreams: analysis.displays.length,
424
+ kind: "inactivity",
425
+ path: analysisPath,
426
+ projectStructureSha256: analysis.projectStructureSha256,
427
+ recommendedRanges: analysis.result.recommendedRanges.length,
428
+ sha256: sha256Hex(`${canonicalJson(analysis)}\n`),
429
+ });
430
+ if (reference.kind !== "inactivity") {
431
+ throw new CliError("internal", "Inactivity reference parsing changed its kind.");
432
+ }
433
+ return reference;
434
+ }
435
+
436
+ export function buildProjectInactivityUpdate(options: {
437
+ readonly analysis: ProjectInactivityAnalysisV1;
438
+ readonly analysisPath: string;
439
+ readonly project: VideoProjectV1;
440
+ readonly updatedAt: string;
441
+ }): {
442
+ readonly project: VideoProjectV1;
443
+ readonly reference: InactivityReference;
444
+ } {
445
+ const analysis = ProjectInactivityAnalysisV1Schema.parse(options.analysis);
446
+ if (options.project.analyses.some(existing => existing.analysisId === analysis.analysisId)) {
447
+ throw new CliError("conflict", `Project analysis already exists: ${analysis.analysisId}`);
448
+ }
449
+ if (Date.parse(options.updatedAt) < Date.parse(options.project.updatedAt)) {
450
+ throw new CliError(
451
+ "conflict",
452
+ "The inactivity analysis update time cannot precede the project update time.",
453
+ );
454
+ }
455
+ const reference = buildProjectInactivityAnalysisReference(
456
+ analysis,
457
+ options.analysisPath,
458
+ );
459
+ const project = VideoProjectV1Schema.parse({
460
+ ...options.project,
461
+ analyses: [...options.project.analyses, reference],
462
+ updatedAt: options.updatedAt,
463
+ });
464
+ return { project, reference };
465
+ }
466
+
467
+ async function syncDirectory(path: string): Promise<void> {
468
+ const handle = await open(path, "r");
469
+ try {
470
+ await handle.sync();
471
+ } finally {
472
+ await handle.close();
473
+ }
474
+ }
475
+
476
+ async function saveProjectInactivityArtifactOnce(
477
+ project: OpenProject,
478
+ analysis: ProjectInactivityAnalysisV1,
479
+ path: string,
480
+ ): Promise<void> {
481
+ const parentRelative = dirname(path);
482
+ const parent = await ensurePhysicalPrivateDirectoryWithin(project.directory.path, parentRelative);
483
+ const stagePath = `${path}.stage-${randomUUID()}`;
484
+ const absoluteStage = join(project.directory.path, stagePath);
485
+ const absoluteTarget = join(project.directory.path, path);
486
+ await saveAnalysisArtifact(project.fileSystem, analysis, stagePath);
487
+ try {
488
+ try {
489
+ await link(absoluteStage, absoluteTarget);
490
+ } catch (error) {
491
+ if (error instanceof Error && "code" in error && error.code === "EEXIST") {
492
+ throw new CliError(
493
+ "conflict",
494
+ `Analysis artifact already exists and will not be overwritten: ${path}`,
495
+ );
496
+ }
497
+ throw error;
498
+ }
499
+ } finally {
500
+ await rm(absoluteStage, { force: true });
501
+ }
502
+ await syncDirectory(parent);
503
+ }
504
+
505
+ export interface PublishedProjectInactivityArtifact {
506
+ readonly analysisPath: string;
507
+ readonly reference: InactivityReference;
508
+ }
509
+
510
+ export async function publishProjectInactivityArtifact(options: {
511
+ readonly analysis: ProjectInactivityAnalysisV1;
512
+ readonly project: OpenProject;
513
+ }): Promise<PublishedProjectInactivityArtifact> {
514
+ const analysis = ProjectInactivityAnalysisV1Schema.parse(options.analysis);
515
+ const analysisPath = projectAnalysisPath("inactivity", analysis.analysisId);
516
+ await requireUnusedAnalysisPath(options.project, analysisPath);
517
+ const reference = buildProjectInactivityAnalysisReference(analysis, analysisPath);
518
+ await saveProjectInactivityArtifactOnce(options.project, analysis, analysisPath);
519
+ const persistedText = await options.project.fileSystem.readText(analysisPath);
520
+ if (sha256Hex(persistedText) !== reference.sha256) {
521
+ throw new CliError(
522
+ "conflict",
523
+ "Persisted inactivity-analysis sidecar failed its content-addressed verification.",
524
+ );
525
+ }
526
+ return { analysisPath, reference };
527
+ }
528
+
529
+ /** Analyze and persist an immutable sidecar plus its validated project reference. */
530
+ export async function analyzeAndPersistProjectInactivity(
531
+ options: AnalyzeProjectInactivityOptions,
532
+ ): Promise<PersistedProjectInactivityAnalysis> {
533
+ const analyzed = await analyzeProjectInactivity(options);
534
+ const published = await publishProjectInactivityArtifact({
535
+ analysis: analyzed.analysis,
536
+ project: options.project,
537
+ });
538
+ const update = buildProjectInactivityUpdate({
539
+ analysis: analyzed.analysis,
540
+ analysisPath: published.analysisPath,
541
+ project: options.project.project,
542
+ updatedAt: analyzed.analysis.createdAt,
543
+ });
544
+ const project = canonicalSlopcameraPersistenceDocument(update.project);
545
+ await saveVideoProject(options.project.fileSystem, project);
546
+ return {
547
+ analysis: analyzed.analysis,
548
+ analysisPath: published.analysisPath,
549
+ ...update,
550
+ project,
551
+ };
552
+ }
@@ -0,0 +1,121 @@
1
+ import { lstat, realpath } from "node:fs/promises";
2
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
3
+
4
+ import { createNodeBundleFileSystem } from "../core/storage";
5
+ import { CliError } from "./errors";
6
+
7
+ export interface ExpectedProjectMediaIntegrity {
8
+ readonly bytes: number;
9
+ readonly sha256: string;
10
+ }
11
+
12
+ export interface PhysicalProjectMediaFingerprint extends ExpectedProjectMediaIntegrity {
13
+ readonly path: string;
14
+ }
15
+
16
+ function isWithin(root: string, candidate: string): boolean {
17
+ const pathFromRoot = relative(root, candidate);
18
+ return pathFromRoot === "" || (
19
+ pathFromRoot !== ".."
20
+ && !pathFromRoot.startsWith(`..${sep}`)
21
+ && !isAbsolute(pathFromRoot)
22
+ );
23
+ }
24
+
25
+ function validateExpectation(expected: ExpectedProjectMediaIntegrity, label: string): void {
26
+ if (!Number.isSafeInteger(expected.bytes) || expected.bytes <= 0) {
27
+ throw new CliError("invalid-data", `${label} has an invalid expected byte length.`);
28
+ }
29
+ if (!/^[a-f0-9]{64}$/u.test(expected.sha256)) {
30
+ throw new CliError("invalid-data", `${label} has an invalid expected SHA-256 digest.`);
31
+ }
32
+ }
33
+
34
+ export async function fingerprintPhysicalProjectMedia(
35
+ path: string,
36
+ maximumBytes: number,
37
+ ): Promise<PhysicalProjectMediaFingerprint> {
38
+ if (!Number.isSafeInteger(maximumBytes) || maximumBytes <= 0) {
39
+ throw new CliError("invalid-data", "Project media fingerprint byte bound must be positive.");
40
+ }
41
+ const lexical = await lstat(path);
42
+ if (lexical.isSymbolicLink() || !lexical.isFile() || lexical.size <= 0 || lexical.size > maximumBytes) {
43
+ throw new CliError("unsafe-path", `Project media must be a bounded physical regular file: ${path}`);
44
+ }
45
+ try {
46
+ // The shared inspector pins one physical descriptor, bounds each read, and
47
+ // accepts a metadata-only invalidation only after a stable second pass
48
+ // reproduces the exact first digest. This never repeats an ingest or render.
49
+ const integrity = await createNodeBundleFileSystem(dirname(path)).inspectFile!(basename(path), maximumBytes);
50
+ if (integrity.bytes !== lexical.size) throw new Error("Project media byte length changed after admission.");
51
+ return { ...integrity, path };
52
+ } catch (cause) {
53
+ throw new CliError("conflict", `Project media changed during validation: ${path}`, { cause });
54
+ }
55
+ }
56
+
57
+ export async function verifyPhysicalProjectMedia(
58
+ path: string,
59
+ expected: ExpectedProjectMediaIntegrity,
60
+ label: string,
61
+ ): Promise<string> {
62
+ validateExpectation(expected, label);
63
+ const details = await lstat(path);
64
+ if (details.isSymbolicLink() || !details.isFile()) {
65
+ throw new CliError("unsafe-path", `${label} must be a physical regular file.`);
66
+ }
67
+ if (details.size !== expected.bytes) {
68
+ throw new CliError(
69
+ "invalid-data",
70
+ `${label} byte length changed: expected ${expected.bytes}, received ${details.size}.`,
71
+ );
72
+ }
73
+ const actual = await fingerprintPhysicalProjectMedia(path, expected.bytes);
74
+ if (actual.bytes !== expected.bytes || actual.sha256 !== expected.sha256) {
75
+ throw new CliError("invalid-data", `${label} failed its recorded SHA-256 integrity check.`);
76
+ }
77
+ return actual.path;
78
+ }
79
+
80
+ export async function resolveVerifiedProjectMedia(options: {
81
+ readonly expected: ExpectedProjectMediaIntegrity;
82
+ readonly label: string;
83
+ readonly path: string;
84
+ readonly repositoryRoot: string;
85
+ }): Promise<string> {
86
+ const actual = await resolvePhysicalProjectMedia(options.repositoryRoot, options.path);
87
+ return await verifyPhysicalProjectMedia(actual, options.expected, options.label);
88
+ }
89
+
90
+ export async function resolvePhysicalProjectMedia(
91
+ repositoryRoot: string,
92
+ path: string,
93
+ ): Promise<string> {
94
+ const root = await realpath(repositoryRoot);
95
+ const lexical = resolve(root, path);
96
+ if (!isWithin(root, lexical)) {
97
+ throw new CliError("unsafe-path", `Project media path escapes the repository: ${path}`);
98
+ }
99
+ const pathFromRoot = relative(root, lexical);
100
+ const parts = pathFromRoot === "" ? [] : pathFromRoot.split(sep);
101
+ if (parts.length === 0) {
102
+ throw new CliError("unsafe-path", `Project media path must identify a file: ${path}`);
103
+ }
104
+ let current = root;
105
+ for (const [index, part] of parts.entries()) {
106
+ current = join(current, part);
107
+ const details = await lstat(current);
108
+ const leaf = index === parts.length - 1;
109
+ if (details.isSymbolicLink() || (leaf ? !details.isFile() : !details.isDirectory())) {
110
+ throw new CliError(
111
+ "unsafe-path",
112
+ `Project media path must use physical directories and a physical regular file: ${path}`,
113
+ );
114
+ }
115
+ }
116
+ const actual = await realpath(lexical);
117
+ if (!isWithin(root, actual)) {
118
+ throw new CliError("unsafe-path", `Project media path resolves outside the repository: ${path}`);
119
+ }
120
+ return actual;
121
+ }