@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.
- package/LICENSE +21 -0
- package/NOTICE.md +60 -0
- package/PRIVACY.md +46 -0
- package/README.md +311 -0
- package/SECURITY.md +21 -0
- package/apps/desktop/README.md +966 -0
- package/apps/desktop/analysis/protocol.ts +223 -0
- package/apps/desktop/application/capability-binding.ts +594 -0
- package/apps/desktop/application/context.ts +97 -0
- package/apps/desktop/application/creative-iteration.ts +1385 -0
- package/apps/desktop/application/default-registry.ts +146 -0
- package/apps/desktop/application/directing-contract.ts +218 -0
- package/apps/desktop/application/directing-plan.ts +178 -0
- package/apps/desktop/application/errors.ts +18 -0
- package/apps/desktop/application/gateway-port.ts +867 -0
- package/apps/desktop/application/html-overlay-browser-runtime.ts +687 -0
- package/apps/desktop/application/html-overlay-integrity.ts +337 -0
- package/apps/desktop/application/html-overlay-renderer.ts +45 -0
- package/apps/desktop/application/index.ts +13 -0
- package/apps/desktop/application/operation-completion-checkpoint.ts +333 -0
- package/apps/desktop/application/operation-effects.ts +94 -0
- package/apps/desktop/application/operation.ts +224 -0
- package/apps/desktop/application/operations/analysis/faces.ts +244 -0
- package/apps/desktop/application/operations/analysis/music.ts +248 -0
- package/apps/desktop/application/operations/analysis/project-auto-zooms.ts +302 -0
- package/apps/desktop/application/operations/analysis/project-inactivity.ts +381 -0
- package/apps/desktop/application/operations/analysis/scenes.ts +272 -0
- package/apps/desktop/application/operations/analysis/shared.ts +202 -0
- package/apps/desktop/application/operations/derive/edit-batch.ts +967 -0
- package/apps/desktop/application/operations/derive/follow-faces.ts +454 -0
- package/apps/desktop/application/operations/edit/bind-candidate-revision.ts +91 -0
- package/apps/desktop/application/operations/edit/create-candidate-revision.ts +377 -0
- package/apps/desktop/application/operations/edit/create-revision.ts +255 -0
- package/apps/desktop/application/operations/edit/freeze-revision.ts +182 -0
- package/apps/desktop/application/operations/gateway/image.ts +41 -0
- package/apps/desktop/application/operations/gateway/index.ts +17 -0
- package/apps/desktop/application/operations/gateway/shared.ts +122 -0
- package/apps/desktop/application/operations/gateway/speech.ts +41 -0
- package/apps/desktop/application/operations/gateway/transcription.ts +42 -0
- package/apps/desktop/application/operations/gateway/video.ts +41 -0
- package/apps/desktop/application/operations/index.ts +31 -0
- package/apps/desktop/application/operations/iteration/create-candidate.ts +240 -0
- package/apps/desktop/application/operations/iteration/create-matrix.ts +117 -0
- package/apps/desktop/application/operations/iteration/select.ts +155 -0
- package/apps/desktop/application/operations/iteration/shared.ts +250 -0
- package/apps/desktop/application/operations/media/audio-effects.ts +376 -0
- package/apps/desktop/application/operations/media/binding.ts +79 -0
- package/apps/desktop/application/operations/media/capabilities.ts +112 -0
- package/apps/desktop/application/operations/media/color-grade.ts +343 -0
- package/apps/desktop/application/operations/media/html-overlay.ts +802 -0
- package/apps/desktop/application/operations/media/index.ts +8 -0
- package/apps/desktop/application/operations/media/ingest.ts +303 -0
- package/apps/desktop/application/operations/media/overlay.ts +729 -0
- package/apps/desktop/application/operations/media/shared.ts +845 -0
- package/apps/desktop/application/operations/media/transform-platform.ts +120 -0
- package/apps/desktop/application/operations/project/commit-edits.ts +2054 -0
- package/apps/desktop/application/operations/project/promote-selection.ts +495 -0
- package/apps/desktop/application/operations/project/snapshot.ts +95 -0
- package/apps/desktop/application/operations/recording/index.ts +5 -0
- package/apps/desktop/application/operations/recording/pause.ts +34 -0
- package/apps/desktop/application/operations/recording/resume.ts +34 -0
- package/apps/desktop/application/operations/recording/shared.ts +199 -0
- package/apps/desktop/application/operations/recording/start.ts +169 -0
- package/apps/desktop/application/operations/recording/stop.ts +34 -0
- package/apps/desktop/application/operations/render/bind-candidate-output.ts +324 -0
- package/apps/desktop/application/operations/render/materialize-selection.ts +322 -0
- package/apps/desktop/application/operations/render/project-caption-plan.ts +287 -0
- package/apps/desktop/application/operations/render/project-plan.ts +939 -0
- package/apps/desktop/application/operations/render/project-spatial-receipt.ts +70 -0
- package/apps/desktop/application/operations/render/project.ts +1922 -0
- package/apps/desktop/application/operations/shared.ts +67 -0
- package/apps/desktop/application/operations/slopcamera-portable.ts +1188 -0
- package/apps/desktop/application/operations/slopcamera-visuals.ts +784 -0
- package/apps/desktop/application/operations/spatial-project.ts +209 -0
- package/apps/desktop/application/operations/spatial-render.ts +415 -0
- package/apps/desktop/application/operations/spatial-scene.ts +97 -0
- package/apps/desktop/application/operations/studio.ts +143 -0
- package/apps/desktop/application/output-publication-lease.ts +163 -0
- package/apps/desktop/application/project-publication-lease.ts +233 -0
- package/apps/desktop/application/project-store.ts +196 -0
- package/apps/desktop/application/receipts.ts +533 -0
- package/apps/desktop/application/registry.ts +182 -0
- package/apps/desktop/application/spatial-assets.ts +675 -0
- package/apps/desktop/application/spatial-compositor-cadence.ts +23 -0
- package/apps/desktop/application/spatial-project-authority.ts +35 -0
- package/apps/desktop/application/spatial-project-lease.ts +126 -0
- package/apps/desktop/application/spatial-project-render-preparation.ts +225 -0
- package/apps/desktop/application/spatial-project-store.ts +286 -0
- package/apps/desktop/application/spatial-render-projection.ts +201 -0
- package/apps/desktop/application/spatial-render.ts +677 -0
- package/apps/desktop/application/spatial-spz.ts +44 -0
- package/apps/desktop/application/spatial-world-import.ts +135 -0
- package/apps/desktop/application/spatial-world-provenance.ts +46 -0
- package/apps/desktop/application/studio-port.ts +58 -0
- package/apps/desktop/application/verified-receipt-reconciliation.ts +1574 -0
- package/apps/desktop/capture/protocol.ts +689 -0
- package/apps/desktop/cli/analyzer.ts +406 -0
- package/apps/desktop/cli/args.ts +3181 -0
- package/apps/desktop/cli/asset-ingest.ts +747 -0
- package/apps/desktop/cli/atomic-render-effects.ts +210 -0
- package/apps/desktop/cli/atomic-render-platform.ts +205 -0
- package/apps/desktop/cli/atomic-render.ts +50 -0
- package/apps/desktop/cli/audio-analysis.ts +252 -0
- package/apps/desktop/cli/bounded-map.ts +39 -0
- package/apps/desktop/cli/bundle-service.ts +256 -0
- package/apps/desktop/cli/capabilities.ts +212 -0
- package/apps/desktop/cli/capture-bundle.ts +1117 -0
- package/apps/desktop/cli/command-host-resources.ts +251 -0
- package/apps/desktop/cli/commands.ts +6836 -0
- package/apps/desktop/cli/directing-blob.ts +291 -0
- package/apps/desktop/cli/directing-media.ts +517 -0
- package/apps/desktop/cli/directing-quote.ts +89 -0
- package/apps/desktop/cli/directing-service.ts +288 -0
- package/apps/desktop/cli/emoji-assets.ts +538 -0
- package/apps/desktop/cli/errors.ts +29 -0
- package/apps/desktop/cli/face-analysis-service.ts +940 -0
- package/apps/desktop/cli/filter-script.ts +76 -0
- package/apps/desktop/cli/gateway-application-port.ts +1142 -0
- package/apps/desktop/cli/gateway-credential.ts +81 -0
- package/apps/desktop/cli/gateway-media-artifacts.ts +723 -0
- package/apps/desktop/cli/gateway-media-catalog.ts +922 -0
- package/apps/desktop/cli/gateway-media-service.ts +3106 -0
- package/apps/desktop/cli/gateway-media-signature.ts +122 -0
- package/apps/desktop/cli/gateway-provider-options.ts +562 -0
- package/apps/desktop/cli/gateway-scene-provider.ts +391 -0
- package/apps/desktop/cli/help.ts +501 -0
- package/apps/desktop/cli/html-overlay-gpu.ts +97 -0
- package/apps/desktop/cli/html-overlay-renderer.ts +2478 -0
- package/apps/desktop/cli/io.ts +290 -0
- package/apps/desktop/cli/main.ts +123 -0
- package/apps/desktop/cli/media-effects-service.ts +885 -0
- package/apps/desktop/cli/media-ingest-model.ts +364 -0
- package/apps/desktop/cli/media-ingest-platform.ts +227 -0
- package/apps/desktop/cli/media-ingest-program.ts +200 -0
- package/apps/desktop/cli/media-ingest.ts +30 -0
- package/apps/desktop/cli/music-analysis-service.ts +434 -0
- package/apps/desktop/cli/mutation-lock.ts +530 -0
- package/apps/desktop/cli/native-assets.d.ts +12 -0
- package/apps/desktop/cli/native-media-runtime.macos.ts +7 -0
- package/apps/desktop/cli/overlay-playback.ts +177 -0
- package/apps/desktop/cli/paths.ts +265 -0
- package/apps/desktop/cli/portable-surface.ts +242 -0
- package/apps/desktop/cli/project-camera-receipt.ts +112 -0
- package/apps/desktop/cli/project-face-camera.ts +394 -0
- package/apps/desktop/cli/project-inactivity-service.ts +552 -0
- package/apps/desktop/cli/project-media-integrity.ts +121 -0
- package/apps/desktop/cli/project-renderer.ts +1526 -0
- package/apps/desktop/cli/project-service.ts +519 -0
- package/apps/desktop/cli/project-state-transaction.ts +520 -0
- package/apps/desktop/cli/query-limits.ts +1 -0
- package/apps/desktop/cli/recording-controller.ts +1567 -0
- package/apps/desktop/cli/recording-daemon.ts +931 -0
- package/apps/desktop/cli/recording-ref.ts +56 -0
- package/apps/desktop/cli/renamed-environment.ts +6 -0
- package/apps/desktop/cli/renderer.ts +1439 -0
- package/apps/desktop/cli/run-cli-test-helper.ts +60 -0
- package/apps/desktop/cli/scene-analysis-service.ts +879 -0
- package/apps/desktop/cli/spatial-compositor-verifier.ts +213 -0
- package/apps/desktop/cli/spatial-project-service.ts +90 -0
- package/apps/desktop/cli/spatial-scene-service.ts +105 -0
- package/apps/desktop/cli/spatial-world-service.ts +39 -0
- package/apps/desktop/cli/speech-analysis-service.ts +682 -0
- package/apps/desktop/cli/studio-assemble.ts +58 -0
- package/apps/desktop/cli/studio-command.ts +60 -0
- package/apps/desktop/cli/studio-custody.ts +60 -0
- package/apps/desktop/cli/studio-encode.ts +216 -0
- package/apps/desktop/cli/studio-exr.ts +52 -0
- package/apps/desktop/cli/studio-files.ts +179 -0
- package/apps/desktop/cli/studio-output-validation.ts +162 -0
- package/apps/desktop/cli/studio-process.ts +145 -0
- package/apps/desktop/cli/studio-runtime.ts +77 -0
- package/apps/desktop/cli/studio-scaffold.ts +70 -0
- package/apps/desktop/cli/studio-service.ts +301 -0
- package/apps/desktop/cli/studio-spatial-asset.ts +248 -0
- package/apps/desktop/cli/studio-template-names.ts +3 -0
- package/apps/desktop/cli/studio-workflow.ts +19 -0
- package/apps/desktop/cli/time.ts +25 -0
- package/apps/desktop/cli/workflow-code.ts +584 -0
- package/apps/desktop/cli/workflow-runs.ts +576 -0
- package/apps/desktop/code/advanced.ts +139 -0
- package/apps/desktop/code/application-node-planner.ts +1288 -0
- package/apps/desktop/code/compiler.ts +329 -0
- package/apps/desktop/code/contracts.ts +13 -0
- package/apps/desktop/code/creative-recipes.ts +94 -0
- package/apps/desktop/code/define-workflow.ts +115 -0
- package/apps/desktop/code/editing.ts +284 -0
- package/apps/desktop/code/file-candidate-provenance.ts +420 -0
- package/apps/desktop/code/file-candidate.ts +69 -0
- package/apps/desktop/code/graph-builder.ts +10 -0
- package/apps/desktop/code/host-resource-policy.ts +55 -0
- package/apps/desktop/code/plan-contracts.ts +159 -0
- package/apps/desktop/code/planning.ts +361 -0
- package/apps/desktop/code/public.ts +134 -0
- package/apps/desktop/code/reference-projection.ts +62 -0
- package/apps/desktop/code/run-contracts.ts +466 -0
- package/apps/desktop/code/run-store.ts +2478 -0
- package/apps/desktop/code/runtime-identity.ts +328 -0
- package/apps/desktop/code/scheduler.ts +3067 -0
- package/apps/desktop/code/semantic-builder.ts +1436 -0
- package/apps/desktop/code/source-bundle.ts +1641 -0
- package/apps/desktop/code/source-typecheck.ts +211 -0
- package/apps/desktop/code/testing.ts +79 -0
- package/apps/desktop/code/worker-client.ts +1675 -0
- package/apps/desktop/code/worker-effects.ts +141 -0
- package/apps/desktop/code/worker-entry.ts +435 -0
- package/apps/desktop/code/worker-lease-guardian.ts +179 -0
- package/apps/desktop/code/worker-process-identity.ts +484 -0
- package/apps/desktop/code/worker-protocol.ts +211 -0
- package/apps/desktop/code/workflow-effects.ts +132 -0
- package/apps/desktop/contracts/analysis.ts +949 -0
- package/apps/desktop/contracts/edit.ts +576 -0
- package/apps/desktop/contracts/index.ts +9 -0
- package/apps/desktop/contracts/media-effects.ts +139 -0
- package/apps/desktop/contracts/project-render.ts +644 -0
- package/apps/desktop/contracts/project.ts +740 -0
- package/apps/desktop/contracts/recording.ts +1434 -0
- package/apps/desktop/contracts/render.ts +334 -0
- package/apps/desktop/contracts/runtime.ts +155 -0
- package/apps/desktop/contracts/spatial-compositor.ts +58 -0
- package/apps/desktop/contracts/spatial-project.ts +159 -0
- package/apps/desktop/contracts/spatial-world.ts +48 -0
- package/apps/desktop/contracts/video-effects.ts +120 -0
- package/apps/desktop/core/alignment-apply.ts +400 -0
- package/apps/desktop/core/audio-alignment.ts +466 -0
- package/apps/desktop/core/auto-zoom.ts +209 -0
- package/apps/desktop/core/canonical-json.ts +14 -0
- package/apps/desktop/core/emoji.ts +210 -0
- package/apps/desktop/core/events.ts +409 -0
- package/apps/desktop/core/face-framing.ts +533 -0
- package/apps/desktop/core/face-tracks.ts +326 -0
- package/apps/desktop/core/inactivity.ts +137 -0
- package/apps/desktop/core/index.ts +30 -0
- package/apps/desktop/core/intervals.ts +123 -0
- package/apps/desktop/core/montage.ts +236 -0
- package/apps/desktop/core/music-analysis.ts +518 -0
- package/apps/desktop/core/plan.ts +244 -0
- package/apps/desktop/core/project-auto-zoom.ts +75 -0
- package/apps/desktop/core/project-camera-spatial.ts +676 -0
- package/apps/desktop/core/project-camera.ts +403 -0
- package/apps/desktop/core/project-captions.ts +888 -0
- package/apps/desktop/core/project-inactivity.ts +125 -0
- package/apps/desktop/core/project-metadata.ts +927 -0
- package/apps/desktop/core/project-plan.ts +375 -0
- package/apps/desktop/core/project-render-plan.ts +303 -0
- package/apps/desktop/core/project-time.ts +469 -0
- package/apps/desktop/core/render-plan.ts +442 -0
- package/apps/desktop/core/scene-cache.ts +174 -0
- package/apps/desktop/core/scene-sampling.ts +448 -0
- package/apps/desktop/core/spatial-compositor.ts +22 -0
- package/apps/desktop/core/spatial-durability.ts +99 -0
- package/apps/desktop/core/spatial-project.ts +169 -0
- package/apps/desktop/core/spatial-shot-clock.ts +32 -0
- package/apps/desktop/core/speech-analysis.ts +333 -0
- package/apps/desktop/core/storage.ts +970 -0
- package/apps/desktop/core/time-map.ts +91 -0
- package/apps/desktop/core/time.ts +61 -0
- package/apps/desktop/core/video-effects.ts +660 -0
- package/apps/desktop/dist/cli/NebulaSans-Bold-26se8aek.otf +0 -0
- package/apps/desktop/dist/cli/NebulaSans-Bold-bcz7y08t.woff2 +0 -0
- package/apps/desktop/dist/cli/NebulaSans-Book-5ax05zvn.woff2 +0 -0
- package/apps/desktop/dist/cli/NebulaSans-Book-8cenzchw.otf +0 -0
- package/apps/desktop/dist/cli/main.js +5086 -0
- package/apps/desktop/html-overlay/catalog.ts +171 -0
- package/apps/desktop/html-overlay/contracts.ts +372 -0
- package/apps/desktop/html-overlay/execution-profile.ts +77 -0
- package/apps/desktop/html-overlay/index.ts +98 -0
- package/apps/desktop/html-overlay/libraries.ts +263 -0
- package/apps/desktop/html-overlay/random.ts +49 -0
- package/apps/desktop/html-overlay/runtime.ts +813 -0
- package/apps/desktop/html-overlay/scaffolds.ts +792 -0
- package/apps/desktop/html-overlay/spark-runtime.ts +88 -0
- package/apps/desktop/html-overlay/spatial-splat-runtime.ts +60 -0
- package/apps/desktop/html-overlay/spatial.ts +720 -0
- package/apps/desktop/studio/assets/README.md +39 -0
- package/apps/desktop/studio/assets/contracts.ts +129 -0
- package/apps/desktop/studio/assets/network.ts +64 -0
- package/apps/desktop/studio/assets/poly-haven.ts +148 -0
- package/apps/desktop/studio/drivers/blender_driver.py +466 -0
- package/apps/desktop/studio/drivers/cadquery_driver.py +176 -0
- package/apps/desktop/studio/education/driver.py +308 -0
- package/apps/desktop/studio/education/lesson.py +180 -0
- package/apps/desktop/studio/education/toolkit.py +249 -0
- package/apps/desktop/workflows/chaptered-demo.ts +48 -0
- package/apps/desktop/workflows/creative-iteration.ts +107 -0
- package/apps/desktop/workflows/creative-selection.ts +92 -0
- package/apps/desktop/workflows/fragments.ts +486 -0
- package/apps/desktop/workflows/index.ts +93 -0
- package/apps/desktop/workflows/polished-screen-demo.ts +128 -0
- package/apps/desktop/workflows/scene-direction.ts +12 -0
- package/apps/desktop/workflows/social-variants.ts +75 -0
- package/apps/desktop/workflows/talking-head-cleanup.ts +53 -0
- package/dist/NebulaSans-Bold-26se8aek.otf +0 -0
- package/dist/NebulaSans-Bold-bcz7y08t.woff2 +0 -0
- package/dist/NebulaSans-Book-5ax05zvn.woff2 +0 -0
- package/dist/NebulaSans-Book-8cenzchw.otf +0 -0
- package/dist/cli.js +479 -0
- package/dist/code/advanced.js +270 -0
- package/dist/code/index.js +2240 -0
- package/dist/generate.js +24 -0
- package/dist/host-resources.js +26 -0
- package/dist/index-42zsesc1.js +2257 -0
- package/dist/index-7308egqr.js +48 -0
- package/dist/index-8txs6fkn.js +712 -0
- package/dist/index-mcy8z0br.js +192 -0
- package/dist/index-p63wavx0.js +2029 -0
- package/dist/index-r7gdhmsp.js +424 -0
- package/dist/index-sh6xbav6.js +1219 -0
- package/dist/index-z1w83f81.js +4 -0
- package/dist/index-z7239b4h.js +2363 -0
- package/dist/index-zfeg9hmw.js +2013 -0
- package/dist/index.js +241 -0
- package/dist/operations.js +29 -0
- package/dist/skill-install-dh0ntqba.js +10 -0
- package/dist/vectorize/worker.js +142 -0
- package/dist/workflow.js +16 -0
- package/docs/README.md +38 -0
- package/docs/studio.md +215 -0
- package/examples/studio/blender/README.md +26 -0
- package/examples/studio/blender/character.py +152 -0
- package/examples/studio/blender/cloth.py +101 -0
- package/examples/studio/blender/color_chart.py +29 -0
- package/examples/studio/blender/fluid.py +78 -0
- package/examples/studio/blender/import_model.py +74 -0
- package/examples/studio/blender/product.py +53 -0
- package/examples/studio/blender/shaded_street.py +481 -0
- package/examples/studio/blender/studio_scene.py +147 -0
- package/examples/studio/cadquery/README.md +13 -0
- package/examples/studio/cadquery/bracket.py +36 -0
- package/examples/studio/cadquery/import_step.py +12 -0
- package/examples/studio/education/README.md +46 -0
- package/examples/studio/education/lesson.json +99 -0
- package/examples/studio/education/scene.py +7 -0
- package/examples/studio/hybrid-scene.ts +96 -0
- package/examples/studio/native-workflow.ts +14 -0
- package/examples/studio/vgpu/README.md +64 -0
- package/examples/studio/vgpu/heat-field.mjs +290 -0
- package/package.json +304 -0
- package/packages/scene/src/index.ts +1 -0
- package/packages/scene/src/scene.ts +215 -0
- package/schema/diagram.schema.json +334 -0
- package/skills/slopcamera/SKILL.md +45 -0
- package/skills/slopcamera/agents/openai.yaml +4 -0
- package/skills/slopcamera/references/customization.md +77 -0
- package/skills/slopcamera/references/diagrams.md +153 -0
- package/skills/slopcamera/references/directed-scenes.md +42 -0
- package/skills/slopcamera/references/directing-video.md +18 -0
- package/skills/slopcamera/references/educational-video.md +26 -0
- package/skills/slopcamera/references/gateway-media.md +134 -0
- package/skills/slopcamera/references/install.md +62 -0
- package/skills/slopcamera/references/native-studio.md +31 -0
- package/skills/slopcamera/references/reference-led-3d.md +140 -0
- package/skills/slopcamera/references/rubber-stamp-examples/poster-example-1.jpg +0 -0
- package/skills/slopcamera/references/rubber-stamp-examples/poster-example-2.jpg +0 -0
- package/skills/slopcamera/references/rubber-stamp-examples/stamp-style-1.png +0 -0
- package/skills/slopcamera/references/rubber-stamp-examples/stamp-style-2.png +0 -0
- package/skills/slopcamera/references/rubber-stamp-field-notes.md +144 -0
- package/skills/slopcamera/references/vectorization.md +29 -0
- package/skills/slopcamera/references/video-projects.md +187 -0
- package/skills/slopcamera/references/visual-communication.md +120 -0
- package/skills/slopcamera/references/workflows-sdk.md +45 -0
- package/skills/slopcamera/scripts/compose-rubber-stamp-field-note.ts +229 -0
- package/src/artifacts.ts +106 -0
- package/src/assets/fonts/nebula-sans/LICENSE.txt +96 -0
- package/src/assets/fonts/nebula-sans/NebulaSans-Bold.otf +0 -0
- package/src/assets/fonts/nebula-sans/NebulaSans-Bold.woff2 +0 -0
- package/src/assets/fonts/nebula-sans/NebulaSans-Book.otf +0 -0
- package/src/assets/fonts/nebula-sans/NebulaSans-Book.woff2 +0 -0
- package/src/assets/fonts/nebula-sans/PROVENANCE.md +21 -0
- package/src/assets.d.ts +9 -0
- package/src/cli.ts +601 -0
- package/src/cloud-errors.ts +26 -0
- package/src/code/advanced.ts +10 -0
- package/src/code/boundary.ts +21 -0
- package/src/code/canonical-json.ts +129 -0
- package/src/code/compiler.ts +953 -0
- package/src/code/contracts.ts +576 -0
- package/src/code/define-workflow.ts +334 -0
- package/src/code/errors.ts +41 -0
- package/src/code/graph-builder.ts +945 -0
- package/src/code/index.ts +121 -0
- package/src/code/json-snapshot.ts +899 -0
- package/src/code/json-utf8.ts +88 -0
- package/src/code/portable-builder.ts +136 -0
- package/src/code/projection.ts +378 -0
- package/src/code/public-operations.ts +450 -0
- package/src/code/runtime.ts +580 -0
- package/src/code/sha256.ts +36 -0
- package/src/config.ts +180 -0
- package/src/desktop.ts +253 -0
- package/src/fs.ts +10 -0
- package/src/generate.ts +704 -0
- package/src/host-resource-posix.ts +161 -0
- package/src/host-resources.ts +1749 -0
- package/src/icons.ts +58 -0
- package/src/index.ts +200 -0
- package/src/label-layout.ts +130 -0
- package/src/layout.ts +413 -0
- package/src/lint.ts +127 -0
- package/src/mcp/boundary.ts +354 -0
- package/src/mcp/index.ts +33 -0
- package/src/mcp/server.ts +272 -0
- package/src/mcp/tools.ts +963 -0
- package/src/mcp/types.ts +71 -0
- package/src/operations.ts +775 -0
- package/src/parse.ts +861 -0
- package/src/process-environment.ts +17 -0
- package/src/render.ts +437 -0
- package/src/skill-install.ts +49 -0
- package/src/spatial-scene/authoring.ts +27 -0
- package/src/spatial-scene/camera-track.ts +83 -0
- package/src/spatial-scene/contracts.ts +277 -0
- package/src/spatial-scene/evaluate.ts +145 -0
- package/src/spatial-scene/gltf.ts +558 -0
- package/src/spatial-scene/identity.ts +260 -0
- package/src/spatial-scene/index.ts +10 -0
- package/src/spatial-scene/inspect.ts +68 -0
- package/src/spatial-scene/math.ts +280 -0
- package/src/spatial-scene/patch.ts +131 -0
- package/src/spatial-scene/time.ts +53 -0
- package/src/studio/contracts.ts +129 -0
- package/src/studio/index.ts +2 -0
- package/src/studio/plan.ts +156 -0
- package/src/studio/shared.ts +41 -0
- package/src/theme.ts +68 -0
- package/src/tldr.ts +474 -0
- package/src/types.ts +212 -0
- package/src/vectorize/archive.ts +101 -0
- package/src/vectorize/command.ts +797 -0
- package/src/vectorize/index.ts +27 -0
- package/src/vectorize/limits.ts +61 -0
- package/src/vectorize/metrics.ts +351 -0
- package/src/vectorize/pixels.ts +323 -0
- package/src/vectorize/supervisor.ts +339 -0
- package/src/vectorize/svg.ts +188 -0
- package/src/vectorize/tool.ts +542 -0
- package/src/vectorize/types.ts +113 -0
- package/src/vectorize/vectorize.ts +634 -0
- package/src/vectorize/worker-protocol.ts +40 -0
- package/src/vectorize/worker.ts +209 -0
- package/src/version.ts +1 -0
- package/src/workflow.ts +409 -0
|
@@ -0,0 +1,953 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
AuthoredWorkflowGraphV1Schema,
|
|
5
|
+
CompiledWorkflowGraphSchema,
|
|
6
|
+
GraphCompilerLimitsSchema,
|
|
7
|
+
MAX_OPERATION_DISCOVERY_ENTRIES,
|
|
8
|
+
MAX_SERIALIZED_GRAPH_NODES,
|
|
9
|
+
OPERATION_PREPARATION_KINDS,
|
|
10
|
+
OPERATION_RESOURCE_KINDS,
|
|
11
|
+
REQUIREMENT_ENVELOPE_VERSION,
|
|
12
|
+
RequirementEnvelopeSchema,
|
|
13
|
+
Sha256Schema,
|
|
14
|
+
UNRESOLVED_REQUIREMENT_KINDS,
|
|
15
|
+
WORKFLOW_COMPILATION_VERSION,
|
|
16
|
+
WORKFLOW_EFFECT_CLASSES,
|
|
17
|
+
WORKFLOW_GRAPH_VERSION,
|
|
18
|
+
WORKFLOW_REF_VERSION,
|
|
19
|
+
WORKFLOW_RESUME_CLASSES,
|
|
20
|
+
isComputeGraphNode,
|
|
21
|
+
isOperationGraphNode,
|
|
22
|
+
trustedComputePolicy,
|
|
23
|
+
type AuthoredGraphNodeV1,
|
|
24
|
+
type AuthoredWorkflowGraphV1,
|
|
25
|
+
type CompiledWorkflowGraph,
|
|
26
|
+
type GraphCompilerLimits,
|
|
27
|
+
type GraphInputValue,
|
|
28
|
+
type OperationDiscoverySource,
|
|
29
|
+
type OperationKind,
|
|
30
|
+
type OperationPreparationKind,
|
|
31
|
+
type OperationResourceKind,
|
|
32
|
+
type RequirementEnvelope,
|
|
33
|
+
type SerializedRefV1,
|
|
34
|
+
type WorkflowEffectClass,
|
|
35
|
+
type WorkflowNodePolicy,
|
|
36
|
+
type WorkflowOutputBinding,
|
|
37
|
+
type WorkflowRegistryProjection,
|
|
38
|
+
type WorkflowResumeClass,
|
|
39
|
+
} from "./contracts.js"
|
|
40
|
+
import {
|
|
41
|
+
canonicalJsonSha256,
|
|
42
|
+
canonicalJsonSha256Prefixed,
|
|
43
|
+
} from "./canonical-json.js"
|
|
44
|
+
import { parseCodeBoundary } from "./boundary.js"
|
|
45
|
+
import { SlopcameraCodeError } from "./errors.js"
|
|
46
|
+
import {
|
|
47
|
+
createBoundedJsonValueSnapshot,
|
|
48
|
+
deepFreezeJson,
|
|
49
|
+
} from "./json-snapshot.js"
|
|
50
|
+
import {
|
|
51
|
+
PUBLIC_WORKFLOW_REGISTRY_PROJECTION,
|
|
52
|
+
createWorkflowRegistryProjection,
|
|
53
|
+
parseWorkflowRegistryProjection,
|
|
54
|
+
} from "./projection.js"
|
|
55
|
+
|
|
56
|
+
export const WORKFLOW_GRAPH_HASH_DOMAIN = "slopcamera.workflow.graph/v1" as const
|
|
57
|
+
export const WORKFLOW_COMPILATION_HASH_DOMAIN =
|
|
58
|
+
"slopcamera.workflow.compilation/v1" as const
|
|
59
|
+
|
|
60
|
+
export const DEFAULT_GRAPH_COMPILER_LIMITS = Object.freeze({
|
|
61
|
+
maxDepth: 64,
|
|
62
|
+
maxEdges: 2_048,
|
|
63
|
+
maxFanOut: 64,
|
|
64
|
+
maxNodes: 256,
|
|
65
|
+
maxTotalOperationFanOut: 4_096,
|
|
66
|
+
}) satisfies GraphCompilerLimits
|
|
67
|
+
|
|
68
|
+
const MAX_SERIALIZED_GRAPH_BYTES = 64 * 1024 * 1024
|
|
69
|
+
const MAX_SERIALIZED_GRAPH_DEPTH = 160
|
|
70
|
+
const MAX_SERIALIZED_GRAPH_VALUES = 1_100_000
|
|
71
|
+
const MAX_GRAPH_COMPILER_LIMIT_BYTES = 1_024
|
|
72
|
+
const MAX_GRAPH_COMPILER_LIMIT_DEPTH = 2
|
|
73
|
+
const MAX_GRAPH_COMPILER_LIMIT_INPUT_VALUES = 16
|
|
74
|
+
// A compilation contains the graph, registry projection, topology, limits,
|
|
75
|
+
// and requirement envelope. Its bounds must compose rather than reuse the
|
|
76
|
+
// graph's exact ceiling.
|
|
77
|
+
const MAX_WORKFLOW_COMPILATION_BYTES = 80 * 1024 * 1024
|
|
78
|
+
const MAX_WORKFLOW_COMPILATION_DEPTH = 192
|
|
79
|
+
const MAX_OPERATION_DISCOVERY_VALUES = 17
|
|
80
|
+
+ OPERATION_PREPARATION_KINDS.length
|
|
81
|
+
+ (3 * OPERATION_RESOURCE_KINDS.length)
|
|
82
|
+
const MAX_WORKFLOW_PROJECTION_VALUES = 5
|
|
83
|
+
+ (MAX_OPERATION_DISCOVERY_ENTRIES * MAX_OPERATION_DISCOVERY_VALUES)
|
|
84
|
+
const MAX_REQUIREMENT_ENVELOPE_VALUES = 21
|
|
85
|
+
+ (3 * MAX_SERIALIZED_GRAPH_NODES)
|
|
86
|
+
+ WORKFLOW_EFFECT_CLASSES.length
|
|
87
|
+
+ OPERATION_PREPARATION_KINDS.length
|
|
88
|
+
+ (3 * OPERATION_RESOURCE_KINDS.length)
|
|
89
|
+
+ WORKFLOW_RESUME_CLASSES.length
|
|
90
|
+
+ UNRESOLVED_REQUIREMENT_KINDS.length
|
|
91
|
+
const MAX_TOPOLOGICAL_WAVE_VALUES = 1 + (2 * MAX_SERIALIZED_GRAPH_NODES)
|
|
92
|
+
const MAX_GRAPH_COMPILER_LIMIT_VALUES = 6
|
|
93
|
+
const MAX_WORKFLOW_COMPILATION_VALUES = MAX_SERIALIZED_GRAPH_VALUES
|
|
94
|
+
+ MAX_WORKFLOW_PROJECTION_VALUES
|
|
95
|
+
+ MAX_REQUIREMENT_ENVELOPE_VALUES
|
|
96
|
+
+ MAX_TOPOLOGICAL_WAVE_VALUES
|
|
97
|
+
+ MAX_GRAPH_COMPILER_LIMIT_VALUES
|
|
98
|
+
+ 4 // Compilation root plus its three scalar fields.
|
|
99
|
+
|
|
100
|
+
export interface CompileWorkflowGraphOptions {
|
|
101
|
+
readonly graph: unknown
|
|
102
|
+
readonly limits?: unknown
|
|
103
|
+
readonly projection?: unknown
|
|
104
|
+
readonly projectionId?: string
|
|
105
|
+
readonly registry?: OperationDiscoverySource
|
|
106
|
+
readonly trustedCompute?: boolean
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface ValidatedGraphTopology {
|
|
110
|
+
readonly depth: number
|
|
111
|
+
readonly edges: number
|
|
112
|
+
readonly structuralFanOut: number
|
|
113
|
+
readonly waves: readonly (readonly string[])[]
|
|
114
|
+
}
|
|
115
|
+
interface ValidatedGraph {
|
|
116
|
+
readonly graph: AuthoredWorkflowGraphV1
|
|
117
|
+
readonly policiesByNode: ReadonlyMap<string, WorkflowNodePolicy>
|
|
118
|
+
readonly topology: ValidatedGraphTopology
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function invalidData(
|
|
122
|
+
message: string,
|
|
123
|
+
details?: Readonly<Record<string, unknown>>,
|
|
124
|
+
): never {
|
|
125
|
+
throw new SlopcameraCodeError("invalid-data", message, details)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function deepFreeze<Value>(value: Value): Value {
|
|
129
|
+
if (typeof value !== "object" || value === null || Object.isFrozen(value)) {
|
|
130
|
+
return value
|
|
131
|
+
}
|
|
132
|
+
for (const nested of Object.values(value)) deepFreeze(nested)
|
|
133
|
+
return Object.freeze(value)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function operationKey(kind: OperationKind, version: number): string {
|
|
137
|
+
return `${kind}@${String(version)}`
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function uniqueSorted<Value extends string>(
|
|
141
|
+
values: Iterable<Value>,
|
|
142
|
+
): readonly Value[] {
|
|
143
|
+
return [...new Set(values)].sort((left, right) => left.localeCompare(right))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function canonicalSlopcameraIdentity(value: string): string {
|
|
147
|
+
if (value === "studio") return "slopcamera"
|
|
148
|
+
return value.replace(/^studio\./u, "slopcamera.")
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function safeAdd(left: number, right: number, name: string): number {
|
|
152
|
+
const result = left + right
|
|
153
|
+
if (!Number.isSafeInteger(result)) {
|
|
154
|
+
return invalidData(`${name} exceeds the safe integer range.`, { left, right })
|
|
155
|
+
}
|
|
156
|
+
return result
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function normalizeLimits(input: unknown): GraphCompilerLimits {
|
|
160
|
+
if (input === undefined) return DEFAULT_GRAPH_COMPILER_LIMITS
|
|
161
|
+
const captured = createBoundedJsonValueSnapshot(
|
|
162
|
+
input,
|
|
163
|
+
MAX_GRAPH_COMPILER_LIMIT_BYTES,
|
|
164
|
+
"graph compiler limits",
|
|
165
|
+
{
|
|
166
|
+
maximumDepth: MAX_GRAPH_COMPILER_LIMIT_DEPTH,
|
|
167
|
+
maximumValues: MAX_GRAPH_COMPILER_LIMIT_INPUT_VALUES,
|
|
168
|
+
},
|
|
169
|
+
).value
|
|
170
|
+
if (typeof captured !== "object" || captured === null || Array.isArray(captured)) {
|
|
171
|
+
return parseCodeBoundary(
|
|
172
|
+
GraphCompilerLimitsSchema,
|
|
173
|
+
captured,
|
|
174
|
+
"graph compiler limits",
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
return parseCodeBoundary(GraphCompilerLimitsSchema, {
|
|
178
|
+
...DEFAULT_GRAPH_COMPILER_LIMITS,
|
|
179
|
+
...captured,
|
|
180
|
+
}, "graph compiler limits")
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function preflightAuthoredWorkflowGraph(
|
|
184
|
+
graphInput: unknown,
|
|
185
|
+
maximumNodes: number,
|
|
186
|
+
): void {
|
|
187
|
+
if (
|
|
188
|
+
typeof graphInput !== "object"
|
|
189
|
+
|| graphInput === null
|
|
190
|
+
|| Array.isArray(graphInput)
|
|
191
|
+
) {
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
const nodes = Object.getOwnPropertyDescriptor(
|
|
195
|
+
graphInput,
|
|
196
|
+
"nodes",
|
|
197
|
+
)?.value as unknown
|
|
198
|
+
if (Array.isArray(nodes) && nodes.length > maximumNodes) {
|
|
199
|
+
return invalidData(
|
|
200
|
+
`Workflow has ${String(nodes.length)} nodes; the limit is ${String(maximumNodes)}.`,
|
|
201
|
+
{ actual: nodes.length, limit: maximumNodes },
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function parseAuthoredWorkflowGraphPreservingIdentity(
|
|
207
|
+
graphInput: unknown,
|
|
208
|
+
maximumNodes = MAX_SERIALIZED_GRAPH_NODES,
|
|
209
|
+
): AuthoredWorkflowGraphV1 {
|
|
210
|
+
preflightAuthoredWorkflowGraph(graphInput, maximumNodes)
|
|
211
|
+
const captured = createBoundedJsonValueSnapshot(
|
|
212
|
+
graphInput,
|
|
213
|
+
MAX_SERIALIZED_GRAPH_BYTES,
|
|
214
|
+
"Authored workflow graph",
|
|
215
|
+
{
|
|
216
|
+
maximumDepth: MAX_SERIALIZED_GRAPH_DEPTH,
|
|
217
|
+
maximumValues: MAX_SERIALIZED_GRAPH_VALUES,
|
|
218
|
+
},
|
|
219
|
+
).value
|
|
220
|
+
const graph = parseCodeBoundary(
|
|
221
|
+
AuthoredWorkflowGraphV1Schema,
|
|
222
|
+
captured,
|
|
223
|
+
"authored workflow graph",
|
|
224
|
+
)
|
|
225
|
+
if (graph.nodes.length > maximumNodes) {
|
|
226
|
+
return invalidData(
|
|
227
|
+
`Workflow has ${String(graph.nodes.length)} nodes; the limit is ${String(maximumNodes)}.`,
|
|
228
|
+
{ actual: graph.nodes.length, limit: maximumNodes },
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
const sorted = {
|
|
232
|
+
...graph,
|
|
233
|
+
nodes: [...graph.nodes].sort((left, right) => (
|
|
234
|
+
left.key.localeCompare(right.key)
|
|
235
|
+
)),
|
|
236
|
+
}
|
|
237
|
+
return deepFreezeJson(sorted)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function canonicalizeGraphValue(
|
|
241
|
+
value: GraphInputValue | WorkflowOutputBinding,
|
|
242
|
+
): GraphInputValue | WorkflowOutputBinding {
|
|
243
|
+
if (isSerializedRef(value)) {
|
|
244
|
+
return {
|
|
245
|
+
$ref: {
|
|
246
|
+
...value.$ref,
|
|
247
|
+
schemaId: canonicalSlopcameraIdentity(value.$ref.schemaId),
|
|
248
|
+
},
|
|
249
|
+
version: WORKFLOW_REF_VERSION,
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (Array.isArray(value)) {
|
|
253
|
+
return value.map(canonicalizeGraphValue) as readonly GraphInputValue[]
|
|
254
|
+
}
|
|
255
|
+
if (typeof value === "object" && value !== null) {
|
|
256
|
+
return Object.fromEntries(Object.entries(value).map(([key, nested]) => (
|
|
257
|
+
[key, canonicalizeGraphValue(nested)]
|
|
258
|
+
))) as GraphInputValue
|
|
259
|
+
}
|
|
260
|
+
return value
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function canonicalizeAuthoredWorkflowGraph(
|
|
264
|
+
graph: AuthoredWorkflowGraphV1,
|
|
265
|
+
): AuthoredWorkflowGraphV1 {
|
|
266
|
+
return deepFreezeJson(parseCodeBoundary(AuthoredWorkflowGraphV1Schema, {
|
|
267
|
+
...graph,
|
|
268
|
+
nodes: graph.nodes.map(node => ({
|
|
269
|
+
...node,
|
|
270
|
+
executor: node.executor.kind === "operation"
|
|
271
|
+
? {
|
|
272
|
+
kind: "operation" as const,
|
|
273
|
+
operation: {
|
|
274
|
+
...node.executor.operation,
|
|
275
|
+
kind: canonicalSlopcameraIdentity(node.executor.operation.kind),
|
|
276
|
+
},
|
|
277
|
+
}
|
|
278
|
+
: {
|
|
279
|
+
compute: {
|
|
280
|
+
...node.executor.compute,
|
|
281
|
+
key: canonicalSlopcameraIdentity(node.executor.compute.key),
|
|
282
|
+
},
|
|
283
|
+
kind: "compute" as const,
|
|
284
|
+
},
|
|
285
|
+
input: canonicalizeGraphValue(node.input),
|
|
286
|
+
inputSchemaId: canonicalSlopcameraIdentity(node.inputSchemaId),
|
|
287
|
+
outputSchemaId: canonicalSlopcameraIdentity(node.outputSchemaId),
|
|
288
|
+
})),
|
|
289
|
+
outputs: canonicalizeGraphValue(graph.outputs),
|
|
290
|
+
version: WORKFLOW_GRAPH_VERSION,
|
|
291
|
+
workflow: {
|
|
292
|
+
...graph.workflow,
|
|
293
|
+
id: canonicalSlopcameraIdentity(graph.workflow.id),
|
|
294
|
+
inputSchemaId: canonicalSlopcameraIdentity(graph.workflow.inputSchemaId),
|
|
295
|
+
},
|
|
296
|
+
}, "canonical authored workflow graph"))
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function normalizeAuthoredWorkflowGraph(
|
|
300
|
+
graphInput: unknown,
|
|
301
|
+
maximumNodes = MAX_SERIALIZED_GRAPH_NODES,
|
|
302
|
+
): AuthoredWorkflowGraphV1 {
|
|
303
|
+
return canonicalizeAuthoredWorkflowGraph(
|
|
304
|
+
parseAuthoredWorkflowGraphPreservingIdentity(graphInput, maximumNodes),
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function workflowGraphHashFromNormalized(
|
|
309
|
+
graph: AuthoredWorkflowGraphV1,
|
|
310
|
+
): string {
|
|
311
|
+
return canonicalJsonSha256Prefixed(
|
|
312
|
+
`${WORKFLOW_GRAPH_HASH_DOMAIN}\0`,
|
|
313
|
+
graph,
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function createWorkflowGraphHash(graphInput: unknown): string {
|
|
318
|
+
const graph = normalizeAuthoredWorkflowGraph(graphInput)
|
|
319
|
+
return workflowGraphHashFromNormalized(graph)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export const createGraphHash = createWorkflowGraphHash
|
|
323
|
+
|
|
324
|
+
function isSerializedRef(value: unknown): value is SerializedRefV1 {
|
|
325
|
+
return typeof value === "object"
|
|
326
|
+
&& value !== null
|
|
327
|
+
&& !Array.isArray(value)
|
|
328
|
+
&& Object.hasOwn(value, "$ref")
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function isGraphValueArray(
|
|
332
|
+
value: GraphInputValue | WorkflowOutputBinding,
|
|
333
|
+
): value is readonly (GraphInputValue | WorkflowOutputBinding)[] {
|
|
334
|
+
return Array.isArray(value)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function collectReferences(
|
|
338
|
+
value: GraphInputValue | WorkflowOutputBinding,
|
|
339
|
+
): readonly SerializedRefV1[] {
|
|
340
|
+
const references: SerializedRefV1[] = []
|
|
341
|
+
const pending: (GraphInputValue | WorkflowOutputBinding)[] = [value]
|
|
342
|
+
while (pending.length > 0) {
|
|
343
|
+
const current = pending.pop()
|
|
344
|
+
if (current === undefined) continue
|
|
345
|
+
if (isSerializedRef(current)) {
|
|
346
|
+
references.push(current)
|
|
347
|
+
continue
|
|
348
|
+
}
|
|
349
|
+
if (isGraphValueArray(current)) {
|
|
350
|
+
for (const item of current) pending.push(item)
|
|
351
|
+
continue
|
|
352
|
+
}
|
|
353
|
+
if (typeof current === "object" && current !== null) {
|
|
354
|
+
for (const item of Object.values(current)) pending.push(item)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return references
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function arraysEqual(left: readonly string[], right: readonly string[]): boolean {
|
|
361
|
+
return left.length === right.length
|
|
362
|
+
&& left.every((value, index) => value === right[index])
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function validateReference(
|
|
366
|
+
reference: SerializedRefV1,
|
|
367
|
+
nodesByKey: ReadonlyMap<string, AuthoredGraphNodeV1>,
|
|
368
|
+
owner: string,
|
|
369
|
+
): void {
|
|
370
|
+
const producer = nodesByKey.get(reference.$ref.nodeKey)
|
|
371
|
+
if (producer === undefined) {
|
|
372
|
+
return invalidData(
|
|
373
|
+
`${owner} contains a dangling reference to ${reference.$ref.nodeKey}.`,
|
|
374
|
+
{ nodeKey: reference.$ref.nodeKey, owner },
|
|
375
|
+
)
|
|
376
|
+
}
|
|
377
|
+
if (producer.outputSchemaId !== reference.$ref.schemaId) {
|
|
378
|
+
return invalidData(
|
|
379
|
+
`${owner} expects schema ${reference.$ref.schemaId} from ${producer.key}, `
|
|
380
|
+
+ `but the producer declares ${producer.outputSchemaId}.`,
|
|
381
|
+
{
|
|
382
|
+
actualSchemaId: producer.outputSchemaId,
|
|
383
|
+
expectedSchemaId: reference.$ref.schemaId,
|
|
384
|
+
nodeKey: producer.key,
|
|
385
|
+
owner,
|
|
386
|
+
},
|
|
387
|
+
)
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function topologicalWaves(
|
|
392
|
+
nodes: readonly AuthoredGraphNodeV1[],
|
|
393
|
+
limits: GraphCompilerLimits,
|
|
394
|
+
): ValidatedGraphTopology {
|
|
395
|
+
const dependencies = new Map<string, ReadonlySet<string>>()
|
|
396
|
+
const dependents = new Map<string, Set<string>>()
|
|
397
|
+
let edgeCount = 0
|
|
398
|
+
for (const node of nodes) {
|
|
399
|
+
const nodeDependencies = new Set(node.dependencies)
|
|
400
|
+
dependencies.set(node.key, nodeDependencies)
|
|
401
|
+
edgeCount = safeAdd(edgeCount, nodeDependencies.size, "Workflow edge count")
|
|
402
|
+
for (const dependency of nodeDependencies) {
|
|
403
|
+
const entries = dependents.get(dependency) ?? new Set<string>()
|
|
404
|
+
entries.add(node.key)
|
|
405
|
+
dependents.set(dependency, entries)
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (edgeCount > limits.maxEdges) {
|
|
409
|
+
return invalidData(
|
|
410
|
+
`Workflow has ${String(edgeCount)} edges; the limit is ${String(limits.maxEdges)}.`,
|
|
411
|
+
{ actual: edgeCount, limit: limits.maxEdges },
|
|
412
|
+
)
|
|
413
|
+
}
|
|
414
|
+
const structuralFanOut = Math.max(
|
|
415
|
+
0,
|
|
416
|
+
...[...dependents.values()].map(entries => entries.size),
|
|
417
|
+
)
|
|
418
|
+
if (structuralFanOut > limits.maxFanOut) {
|
|
419
|
+
return invalidData(
|
|
420
|
+
`Workflow fan-out is ${String(structuralFanOut)}; the limit is ${String(limits.maxFanOut)}.`,
|
|
421
|
+
{ actual: structuralFanOut, limit: limits.maxFanOut },
|
|
422
|
+
)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
let ready = nodes
|
|
426
|
+
.filter(node => (dependencies.get(node.key)?.size ?? 0) === 0)
|
|
427
|
+
.map(node => node.key)
|
|
428
|
+
.sort((left, right) => left.localeCompare(right))
|
|
429
|
+
const remaining = new Map<string, number>(
|
|
430
|
+
nodes.map(node => [node.key, dependencies.get(node.key)?.size ?? 0]),
|
|
431
|
+
)
|
|
432
|
+
const waves: string[][] = []
|
|
433
|
+
let visited = 0
|
|
434
|
+
while (ready.length > 0) {
|
|
435
|
+
const wave = ready
|
|
436
|
+
waves.push(wave)
|
|
437
|
+
visited += wave.length
|
|
438
|
+
const next = new Set<string>()
|
|
439
|
+
for (const nodeKey of wave) {
|
|
440
|
+
for (const dependent of dependents.get(nodeKey) ?? []) {
|
|
441
|
+
const nextCount = (remaining.get(dependent) ?? 0) - 1
|
|
442
|
+
remaining.set(dependent, nextCount)
|
|
443
|
+
if (nextCount === 0) next.add(dependent)
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
ready = [...next].sort((left, right) => left.localeCompare(right))
|
|
447
|
+
}
|
|
448
|
+
if (visited !== nodes.length) {
|
|
449
|
+
return invalidData("Workflow graph contains a dependency cycle.")
|
|
450
|
+
}
|
|
451
|
+
if (waves.length > limits.maxDepth) {
|
|
452
|
+
return invalidData(
|
|
453
|
+
`Workflow depth is ${String(waves.length)}; the limit is ${String(limits.maxDepth)}.`,
|
|
454
|
+
{ actual: waves.length, limit: limits.maxDepth },
|
|
455
|
+
)
|
|
456
|
+
}
|
|
457
|
+
return {
|
|
458
|
+
depth: waves.length,
|
|
459
|
+
edges: edgeCount,
|
|
460
|
+
structuralFanOut,
|
|
461
|
+
waves,
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function validateGraph(
|
|
466
|
+
graphInput: unknown,
|
|
467
|
+
projection: WorkflowRegistryProjection,
|
|
468
|
+
limits: GraphCompilerLimits,
|
|
469
|
+
): ValidatedGraph {
|
|
470
|
+
const graph = normalizeAuthoredWorkflowGraph(graphInput, limits.maxNodes)
|
|
471
|
+
if (graph.nodes.length > limits.maxNodes) {
|
|
472
|
+
return invalidData(
|
|
473
|
+
`Workflow has ${String(graph.nodes.length)} nodes; the limit is ${String(limits.maxNodes)}.`,
|
|
474
|
+
{ actual: graph.nodes.length, limit: limits.maxNodes },
|
|
475
|
+
)
|
|
476
|
+
}
|
|
477
|
+
const nodesByKey = new Map<string, AuthoredGraphNodeV1>()
|
|
478
|
+
for (const node of graph.nodes) {
|
|
479
|
+
if (nodesByKey.has(node.key)) {
|
|
480
|
+
return invalidData(`Duplicate workflow node key: ${node.key}`, {
|
|
481
|
+
nodeKey: node.key,
|
|
482
|
+
})
|
|
483
|
+
}
|
|
484
|
+
nodesByKey.set(node.key, node)
|
|
485
|
+
}
|
|
486
|
+
const discoveryByKey = new Map(
|
|
487
|
+
projection.discovery.map(operation => [
|
|
488
|
+
operationKey(operation.kind, operation.version),
|
|
489
|
+
operation,
|
|
490
|
+
]),
|
|
491
|
+
)
|
|
492
|
+
const policiesByNode = new Map<string, WorkflowNodePolicy>()
|
|
493
|
+
let totalOperationFanOut = 0
|
|
494
|
+
for (const node of graph.nodes) {
|
|
495
|
+
let policy: WorkflowNodePolicy
|
|
496
|
+
if (isOperationGraphNode(node)) {
|
|
497
|
+
const identity = node.executor.operation
|
|
498
|
+
const operation = discoveryByKey.get(
|
|
499
|
+
operationKey(identity.kind, identity.version),
|
|
500
|
+
)
|
|
501
|
+
if (operation === undefined) {
|
|
502
|
+
throw new SlopcameraCodeError(
|
|
503
|
+
"unsupported-plan",
|
|
504
|
+
`Unsupported operation: ${operationKey(identity.kind, identity.version)}`,
|
|
505
|
+
{
|
|
506
|
+
kind: identity.kind,
|
|
507
|
+
nodeKey: node.key,
|
|
508
|
+
projectionId: projection.id,
|
|
509
|
+
version: identity.version,
|
|
510
|
+
},
|
|
511
|
+
)
|
|
512
|
+
}
|
|
513
|
+
if (
|
|
514
|
+
node.inputSchemaId !== operation.inputSchemaId
|
|
515
|
+
|| node.outputSchemaId !== operation.outputSchemaId
|
|
516
|
+
) {
|
|
517
|
+
return invalidData(
|
|
518
|
+
`Operation schema identity mismatch for ${operationKey(identity.kind, identity.version)} `
|
|
519
|
+
+ `at node ${node.key}.`,
|
|
520
|
+
{
|
|
521
|
+
actualInputSchemaId: node.inputSchemaId,
|
|
522
|
+
actualOutputSchemaId: node.outputSchemaId,
|
|
523
|
+
expectedInputSchemaId: operation.inputSchemaId,
|
|
524
|
+
expectedOutputSchemaId: operation.outputSchemaId,
|
|
525
|
+
kind: identity.kind,
|
|
526
|
+
nodeKey: node.key,
|
|
527
|
+
version: identity.version,
|
|
528
|
+
},
|
|
529
|
+
)
|
|
530
|
+
}
|
|
531
|
+
policy = operation.policy
|
|
532
|
+
} else if (isComputeGraphNode(node)) {
|
|
533
|
+
if (!projection.trustedCompute) {
|
|
534
|
+
throw new SlopcameraCodeError(
|
|
535
|
+
"unsupported-plan",
|
|
536
|
+
`Trusted compute is unsupported at node ${node.key}.`,
|
|
537
|
+
{
|
|
538
|
+
executorKind: "compute",
|
|
539
|
+
nodeKey: node.key,
|
|
540
|
+
projectionId: projection.id,
|
|
541
|
+
},
|
|
542
|
+
)
|
|
543
|
+
}
|
|
544
|
+
policy = trustedComputePolicy(node.executor.compute)
|
|
545
|
+
} else {
|
|
546
|
+
throw new SlopcameraCodeError(
|
|
547
|
+
"internal",
|
|
548
|
+
`Unknown node executor for ${node.key}.`,
|
|
549
|
+
)
|
|
550
|
+
}
|
|
551
|
+
policiesByNode.set(node.key, policy)
|
|
552
|
+
totalOperationFanOut = safeAdd(
|
|
553
|
+
totalOperationFanOut,
|
|
554
|
+
policy.maxFanOut,
|
|
555
|
+
"Total operation fan-out",
|
|
556
|
+
)
|
|
557
|
+
const references = collectReferences(node.input)
|
|
558
|
+
for (const reference of references) {
|
|
559
|
+
validateReference(reference, nodesByKey, `Node ${node.key}`)
|
|
560
|
+
}
|
|
561
|
+
const controlDependencies = node.controlDependencies ?? []
|
|
562
|
+
const normalizedControlDependencies = uniqueSorted(controlDependencies)
|
|
563
|
+
if (!arraysEqual(controlDependencies, normalizedControlDependencies)) {
|
|
564
|
+
return invalidData(
|
|
565
|
+
`Node ${node.key} control dependencies must be unique and sorted.`,
|
|
566
|
+
{ nodeKey: node.key },
|
|
567
|
+
)
|
|
568
|
+
}
|
|
569
|
+
for (const dependency of controlDependencies) {
|
|
570
|
+
if (!nodesByKey.has(dependency)) {
|
|
571
|
+
return invalidData(
|
|
572
|
+
`Node ${node.key} contains a dangling control dependency to ${dependency}.`,
|
|
573
|
+
{ dependency, nodeKey: node.key },
|
|
574
|
+
)
|
|
575
|
+
}
|
|
576
|
+
if (dependency === node.key) {
|
|
577
|
+
return invalidData(`Node ${node.key} cannot depend on itself.`, {
|
|
578
|
+
nodeKey: node.key,
|
|
579
|
+
})
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
const inferredDependencies = uniqueSorted([
|
|
583
|
+
...references.map(reference => reference.$ref.nodeKey),
|
|
584
|
+
...controlDependencies,
|
|
585
|
+
])
|
|
586
|
+
if (!arraysEqual(node.dependencies, inferredDependencies)) {
|
|
587
|
+
return invalidData(
|
|
588
|
+
`Node ${node.key} dependencies do not match its embedded references and explicit control dependencies. `
|
|
589
|
+
+ `Expected [${inferredDependencies.join(", ")}].`,
|
|
590
|
+
{ expectedDependencies: inferredDependencies, nodeKey: node.key },
|
|
591
|
+
)
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
if (totalOperationFanOut > limits.maxTotalOperationFanOut) {
|
|
595
|
+
return invalidData(
|
|
596
|
+
`Workflow operation fan-out bound is ${String(totalOperationFanOut)}; `
|
|
597
|
+
+ `the limit is ${String(limits.maxTotalOperationFanOut)}.`,
|
|
598
|
+
{ actual: totalOperationFanOut, limit: limits.maxTotalOperationFanOut },
|
|
599
|
+
)
|
|
600
|
+
}
|
|
601
|
+
for (const reference of collectReferences(graph.outputs)) {
|
|
602
|
+
validateReference(reference, nodesByKey, "Workflow outputs")
|
|
603
|
+
}
|
|
604
|
+
return {
|
|
605
|
+
graph,
|
|
606
|
+
policiesByNode,
|
|
607
|
+
topology: topologicalWaves(graph.nodes, limits),
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function operationFamily(kind: OperationKind): string {
|
|
612
|
+
const separator = kind.indexOf(".")
|
|
613
|
+
if (separator < 1) {
|
|
614
|
+
throw new SlopcameraCodeError(
|
|
615
|
+
"internal",
|
|
616
|
+
`Operation ${kind} has no namespace family.`,
|
|
617
|
+
{ kind },
|
|
618
|
+
)
|
|
619
|
+
}
|
|
620
|
+
return kind.slice(0, separator)
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function addResource(
|
|
624
|
+
resources: Map<OperationResourceKind, number>,
|
|
625
|
+
resource: OperationResourceKind,
|
|
626
|
+
amount: number,
|
|
627
|
+
): void {
|
|
628
|
+
resources.set(
|
|
629
|
+
resource,
|
|
630
|
+
safeAdd(resources.get(resource) ?? 0, amount, `Resource ${resource} claim`),
|
|
631
|
+
)
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function deriveRequirementEnvelope(validated: ValidatedGraph): RequirementEnvelope {
|
|
635
|
+
const computeKeys = new Set<string>()
|
|
636
|
+
const effects = new Set<WorkflowEffectClass>()
|
|
637
|
+
const families = new Set<string>()
|
|
638
|
+
const operationKinds = new Set<OperationKind>()
|
|
639
|
+
const preparation = new Set<OperationPreparationKind>()
|
|
640
|
+
const resources = new Map<OperationResourceKind, number>()
|
|
641
|
+
const resumeClasses = new Set<WorkflowResumeClass>()
|
|
642
|
+
let localMediaUploadNodes = 0
|
|
643
|
+
let maxDurationMs = 0
|
|
644
|
+
let maxInputBytes = 0
|
|
645
|
+
let maxOutputBytes = 0
|
|
646
|
+
let totalOperationFanOut = 0
|
|
647
|
+
for (const node of validated.graph.nodes) {
|
|
648
|
+
const policy = validated.policiesByNode.get(node.key)
|
|
649
|
+
if (policy === undefined) {
|
|
650
|
+
throw new SlopcameraCodeError(
|
|
651
|
+
"internal",
|
|
652
|
+
`Node ${node.key} lost its execution policy.`,
|
|
653
|
+
)
|
|
654
|
+
}
|
|
655
|
+
effects.add(policy.effect)
|
|
656
|
+
resumeClasses.add(policy.resume)
|
|
657
|
+
if (isOperationGraphNode(node)) {
|
|
658
|
+
families.add(operationFamily(node.executor.operation.kind))
|
|
659
|
+
operationKinds.add(node.executor.operation.kind)
|
|
660
|
+
} else if (isComputeGraphNode(node)) {
|
|
661
|
+
families.add("compute")
|
|
662
|
+
computeKeys.add(node.executor.compute.key)
|
|
663
|
+
}
|
|
664
|
+
for (const requirement of policy.preparation) preparation.add(requirement)
|
|
665
|
+
for (const claim of policy.resources) {
|
|
666
|
+
addResource(resources, claim.resource, claim.amount)
|
|
667
|
+
}
|
|
668
|
+
if (policy.effect === "paid-cloud" && policy.preparation.includes("local-media")) {
|
|
669
|
+
localMediaUploadNodes = safeAdd(
|
|
670
|
+
localMediaUploadNodes,
|
|
671
|
+
1,
|
|
672
|
+
"Local media upload node count",
|
|
673
|
+
)
|
|
674
|
+
}
|
|
675
|
+
maxDurationMs = safeAdd(maxDurationMs, policy.maxDurationMs, "Workflow duration bound")
|
|
676
|
+
maxInputBytes = safeAdd(maxInputBytes, policy.maxInputBytes, "Workflow input byte bound")
|
|
677
|
+
maxOutputBytes = safeAdd(maxOutputBytes, policy.maxOutputBytes, "Workflow output byte bound")
|
|
678
|
+
totalOperationFanOut = safeAdd(
|
|
679
|
+
totalOperationFanOut,
|
|
680
|
+
policy.maxFanOut,
|
|
681
|
+
"Workflow operation fan-out bound",
|
|
682
|
+
)
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const unresolved = new Set<RequirementEnvelope["unresolved"][number]>()
|
|
686
|
+
if (validated.topology.edges > 0) unresolved.add("dependency-outputs")
|
|
687
|
+
if (
|
|
688
|
+
preparation.has("local-media")
|
|
689
|
+
|| preparation.has("project-state")
|
|
690
|
+
|| preparation.has("recording-metadata")
|
|
691
|
+
) {
|
|
692
|
+
unresolved.add("exact-media-hashes")
|
|
693
|
+
}
|
|
694
|
+
if (
|
|
695
|
+
preparation.has("recording-metadata")
|
|
696
|
+
|| preparation.has("typed-text")
|
|
697
|
+
|| preparation.has("window-metadata")
|
|
698
|
+
) {
|
|
699
|
+
unresolved.add("exact-sensitive-metadata-hashes")
|
|
700
|
+
}
|
|
701
|
+
if (effects.has("paid-cloud") || resources.has("paid-call")) {
|
|
702
|
+
unresolved.add("prices")
|
|
703
|
+
}
|
|
704
|
+
if (
|
|
705
|
+
effects.has("project-mutation")
|
|
706
|
+
|| operationKinds.has("render.project")
|
|
707
|
+
|| operationKinds.has("render.project-plan")
|
|
708
|
+
) {
|
|
709
|
+
unresolved.add("project-generations")
|
|
710
|
+
}
|
|
711
|
+
if (families.has("render")) unresolved.add("render-plans")
|
|
712
|
+
|
|
713
|
+
return parseCodeBoundary(RequirementEnvelopeSchema, {
|
|
714
|
+
bounds: {
|
|
715
|
+
depth: validated.topology.depth,
|
|
716
|
+
edges: validated.topology.edges,
|
|
717
|
+
localMediaUploadNodes,
|
|
718
|
+
maxDurationMs,
|
|
719
|
+
maxInputBytes,
|
|
720
|
+
maxOutputBytes,
|
|
721
|
+
nodes: validated.graph.nodes.length,
|
|
722
|
+
paidCallClaims: resources.get("paid-call") ?? 0,
|
|
723
|
+
structuralFanOut: validated.topology.structuralFanOut,
|
|
724
|
+
totalOperationFanOut,
|
|
725
|
+
},
|
|
726
|
+
computeKeys: uniqueSorted(computeKeys),
|
|
727
|
+
effects: uniqueSorted(effects),
|
|
728
|
+
operationFamilies: uniqueSorted(families),
|
|
729
|
+
operationKinds: uniqueSorted(operationKinds),
|
|
730
|
+
preparation: uniqueSorted(preparation),
|
|
731
|
+
resources: [...resources]
|
|
732
|
+
.map(([resource, amount]) => ({ amount, resource }))
|
|
733
|
+
.sort((left, right) => (
|
|
734
|
+
left.resource.localeCompare(right.resource)
|
|
735
|
+
)),
|
|
736
|
+
resumeClasses: uniqueSorted(resumeClasses),
|
|
737
|
+
unresolved: uniqueSorted(unresolved),
|
|
738
|
+
version: REQUIREMENT_ENVELOPE_VERSION,
|
|
739
|
+
}, "workflow requirement envelope")
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function canonicalizeRequirementEnvelope(
|
|
743
|
+
input: unknown,
|
|
744
|
+
): RequirementEnvelope {
|
|
745
|
+
const envelope = parseCodeBoundary(
|
|
746
|
+
RequirementEnvelopeSchema,
|
|
747
|
+
input,
|
|
748
|
+
"workflow requirement envelope",
|
|
749
|
+
)
|
|
750
|
+
return parseCodeBoundary(RequirementEnvelopeSchema, {
|
|
751
|
+
...envelope,
|
|
752
|
+
computeKeys: envelope.computeKeys.map(canonicalSlopcameraIdentity),
|
|
753
|
+
operationFamilies: envelope.operationFamilies.map(canonicalSlopcameraIdentity),
|
|
754
|
+
operationKinds: envelope.operationKinds.map(canonicalSlopcameraIdentity),
|
|
755
|
+
version: REQUIREMENT_ENVELOPE_VERSION,
|
|
756
|
+
}, "canonical workflow requirement envelope")
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function resolveProjection(
|
|
760
|
+
options: CompileWorkflowGraphOptions,
|
|
761
|
+
): WorkflowRegistryProjection {
|
|
762
|
+
if (options.projection !== undefined) {
|
|
763
|
+
if (
|
|
764
|
+
options.registry !== undefined
|
|
765
|
+
|| options.projectionId !== undefined
|
|
766
|
+
|| options.trustedCompute !== undefined
|
|
767
|
+
) {
|
|
768
|
+
throw new SlopcameraCodeError(
|
|
769
|
+
"invalid-data",
|
|
770
|
+
"Compile with either a projection or a registry projection source, not both.",
|
|
771
|
+
)
|
|
772
|
+
}
|
|
773
|
+
return parseWorkflowRegistryProjection(options.projection)
|
|
774
|
+
}
|
|
775
|
+
if (options.registry !== undefined) {
|
|
776
|
+
if (options.projectionId === undefined) {
|
|
777
|
+
throw new SlopcameraCodeError(
|
|
778
|
+
"invalid-data",
|
|
779
|
+
"A registry projection source requires an explicit projection id.",
|
|
780
|
+
)
|
|
781
|
+
}
|
|
782
|
+
return createWorkflowRegistryProjection(
|
|
783
|
+
options.projectionId,
|
|
784
|
+
options.registry,
|
|
785
|
+
{ trustedCompute: options.trustedCompute ?? false },
|
|
786
|
+
)
|
|
787
|
+
}
|
|
788
|
+
if (options.projectionId !== undefined || options.trustedCompute !== undefined) {
|
|
789
|
+
throw new SlopcameraCodeError(
|
|
790
|
+
"invalid-data",
|
|
791
|
+
"A projection id or trusted-compute authority requires a registry projection source.",
|
|
792
|
+
)
|
|
793
|
+
}
|
|
794
|
+
return PUBLIC_WORKFLOW_REGISTRY_PROJECTION
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
type UnsignedWorkflowCompilation = Omit<CompiledWorkflowGraph, "compilationSha256">
|
|
798
|
+
|
|
799
|
+
const RequiredCompilationComponentSchema = z.unknown().refine(
|
|
800
|
+
value => value !== undefined,
|
|
801
|
+
"Required",
|
|
802
|
+
)
|
|
803
|
+
const ShallowCompiledWorkflowGraphSchema = z.strictObject({
|
|
804
|
+
compilationSha256: Sha256Schema,
|
|
805
|
+
envelope: RequiredCompilationComponentSchema,
|
|
806
|
+
graph: RequiredCompilationComponentSchema,
|
|
807
|
+
graphSha256: Sha256Schema,
|
|
808
|
+
limits: RequiredCompilationComponentSchema,
|
|
809
|
+
projection: RequiredCompilationComponentSchema,
|
|
810
|
+
topologicalWaves: RequiredCompilationComponentSchema,
|
|
811
|
+
version: z.literal(WORKFLOW_COMPILATION_VERSION),
|
|
812
|
+
})
|
|
813
|
+
|
|
814
|
+
function boundedCompilationInput(input: unknown, name: string): unknown {
|
|
815
|
+
return createBoundedJsonValueSnapshot(
|
|
816
|
+
input,
|
|
817
|
+
MAX_WORKFLOW_COMPILATION_BYTES,
|
|
818
|
+
name,
|
|
819
|
+
{
|
|
820
|
+
maximumDepth: MAX_WORKFLOW_COMPILATION_DEPTH,
|
|
821
|
+
maximumValues: MAX_WORKFLOW_COMPILATION_VALUES,
|
|
822
|
+
},
|
|
823
|
+
).value
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function workflowCompilationHashFromValidated(compilation: unknown): string {
|
|
827
|
+
return canonicalJsonSha256Prefixed(
|
|
828
|
+
`${WORKFLOW_COMPILATION_HASH_DOMAIN}\0`,
|
|
829
|
+
compilation,
|
|
830
|
+
)
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
export function createWorkflowCompilationHash(
|
|
834
|
+
compilationInput: unknown,
|
|
835
|
+
): string {
|
|
836
|
+
const bounded = boundedCompilationInput(
|
|
837
|
+
compilationInput,
|
|
838
|
+
"workflow compilation",
|
|
839
|
+
)
|
|
840
|
+
if (typeof bounded !== "object" || bounded === null || Array.isArray(bounded)) {
|
|
841
|
+
return invalidData("Workflow compilation must be a plain object.")
|
|
842
|
+
}
|
|
843
|
+
const {
|
|
844
|
+
compilationSha256: ignoredCompilationSha256,
|
|
845
|
+
...unsigned
|
|
846
|
+
} = bounded as Record<string, unknown>
|
|
847
|
+
void ignoredCompilationSha256
|
|
848
|
+
const parsed = parseCodeBoundary(
|
|
849
|
+
CompiledWorkflowGraphSchema.omit({ compilationSha256: true }),
|
|
850
|
+
unsigned,
|
|
851
|
+
"unsigned workflow compilation",
|
|
852
|
+
)
|
|
853
|
+
return workflowCompilationHashFromValidated(parsed)
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
export function compileWorkflowGraph(
|
|
857
|
+
options: CompileWorkflowGraphOptions,
|
|
858
|
+
): CompiledWorkflowGraph {
|
|
859
|
+
const limits = normalizeLimits(options.limits)
|
|
860
|
+
const projection = resolveProjection(options)
|
|
861
|
+
const validated = validateGraph(options.graph, projection, limits)
|
|
862
|
+
const graphSha256 = workflowGraphHashFromNormalized(validated.graph)
|
|
863
|
+
const unsigned: UnsignedWorkflowCompilation = {
|
|
864
|
+
envelope: deriveRequirementEnvelope(validated),
|
|
865
|
+
graph: validated.graph,
|
|
866
|
+
graphSha256,
|
|
867
|
+
limits,
|
|
868
|
+
projection,
|
|
869
|
+
topologicalWaves: validated.topology.waves,
|
|
870
|
+
version: WORKFLOW_COMPILATION_VERSION,
|
|
871
|
+
}
|
|
872
|
+
return deepFreeze(parseCodeBoundary(CompiledWorkflowGraphSchema, {
|
|
873
|
+
...unsigned,
|
|
874
|
+
compilationSha256: workflowCompilationHashFromValidated(unsigned),
|
|
875
|
+
}, "compiled workflow graph"))
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
export function parseCompiledWorkflowGraph(input: unknown): CompiledWorkflowGraph {
|
|
879
|
+
const bounded = boundedCompilationInput(input, "compiled workflow graph")
|
|
880
|
+
const shallow = parseCodeBoundary(
|
|
881
|
+
ShallowCompiledWorkflowGraphSchema,
|
|
882
|
+
bounded,
|
|
883
|
+
"compiled workflow graph",
|
|
884
|
+
)
|
|
885
|
+
const {
|
|
886
|
+
compilationSha256: parsedCompilationSha256,
|
|
887
|
+
...unsigned
|
|
888
|
+
} = shallow
|
|
889
|
+
const expected = workflowCompilationHashFromValidated(unsigned)
|
|
890
|
+
if (parsedCompilationSha256 !== expected) {
|
|
891
|
+
throw new SlopcameraCodeError(
|
|
892
|
+
"invalid-data",
|
|
893
|
+
"Workflow compilation hash does not match its contents.",
|
|
894
|
+
{
|
|
895
|
+
actualCompilationSha256: parsedCompilationSha256,
|
|
896
|
+
expectedCompilationSha256: expected,
|
|
897
|
+
},
|
|
898
|
+
)
|
|
899
|
+
}
|
|
900
|
+
const parsed = parseCodeBoundary(
|
|
901
|
+
CompiledWorkflowGraphSchema,
|
|
902
|
+
bounded,
|
|
903
|
+
"compiled workflow graph",
|
|
904
|
+
)
|
|
905
|
+
const exactGraph = parseAuthoredWorkflowGraphPreservingIdentity(parsed.graph)
|
|
906
|
+
if (canonicalJsonSha256(parsed.graph) !== canonicalJsonSha256(exactGraph)) {
|
|
907
|
+
throw new SlopcameraCodeError(
|
|
908
|
+
"invalid-data",
|
|
909
|
+
"Compiled workflow graph nodes are not normalized.",
|
|
910
|
+
)
|
|
911
|
+
}
|
|
912
|
+
const expectedGraphSha256 = workflowGraphHashFromNormalized(exactGraph)
|
|
913
|
+
if (parsed.graphSha256 !== expectedGraphSha256) {
|
|
914
|
+
throw new SlopcameraCodeError(
|
|
915
|
+
"invalid-data",
|
|
916
|
+
"Workflow graph hash does not match its authenticated contents.",
|
|
917
|
+
{
|
|
918
|
+
actualGraphSha256: parsed.graphSha256,
|
|
919
|
+
expectedGraphSha256,
|
|
920
|
+
},
|
|
921
|
+
)
|
|
922
|
+
}
|
|
923
|
+
const canonicalGraph = canonicalizeAuthoredWorkflowGraph(exactGraph)
|
|
924
|
+
const canonicalProjection = parseWorkflowRegistryProjection(parsed.projection)
|
|
925
|
+
const canonicalLimits = normalizeLimits(parsed.limits)
|
|
926
|
+
const recompiled = compileWorkflowGraph({
|
|
927
|
+
graph: canonicalGraph,
|
|
928
|
+
limits: canonicalLimits,
|
|
929
|
+
projection: canonicalProjection,
|
|
930
|
+
})
|
|
931
|
+
const canonicalUnsigned: UnsignedWorkflowCompilation = {
|
|
932
|
+
envelope: canonicalizeRequirementEnvelope(parsed.envelope),
|
|
933
|
+
graph: canonicalGraph,
|
|
934
|
+
graphSha256: workflowGraphHashFromNormalized(canonicalGraph),
|
|
935
|
+
limits: canonicalLimits,
|
|
936
|
+
projection: canonicalProjection,
|
|
937
|
+
topologicalWaves: parsed.topologicalWaves,
|
|
938
|
+
version: WORKFLOW_COMPILATION_VERSION,
|
|
939
|
+
}
|
|
940
|
+
// The predecessor bytes and their nested graph/projection hashes were
|
|
941
|
+
// verified before identity normalization. Recompilation must then match the
|
|
942
|
+
// complete canonicalized semantics rather than the obsolete outer digest.
|
|
943
|
+
if (
|
|
944
|
+
recompiled.compilationSha256
|
|
945
|
+
!== workflowCompilationHashFromValidated(canonicalUnsigned)
|
|
946
|
+
) {
|
|
947
|
+
throw new SlopcameraCodeError(
|
|
948
|
+
"invalid-data",
|
|
949
|
+
"Workflow compilation topology, requirements, or projection do not match the graph.",
|
|
950
|
+
)
|
|
951
|
+
}
|
|
952
|
+
return recompiled
|
|
953
|
+
}
|