@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,1188 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PORTABLE_SLOPCAMERA_OPERATION_CONTRACTS,
|
|
3
|
+
PORTABLE_SLOPCAMERA_OPERATION_KINDS,
|
|
4
|
+
PUBLIC_WORKFLOW_REGISTRY_PROJECTION,
|
|
5
|
+
type PortableSlopcameraOperationKind,
|
|
6
|
+
} from "@hraness/slopcamera/code/advanced";
|
|
7
|
+
import {
|
|
8
|
+
executeSlopcameraOperation,
|
|
9
|
+
executeSlopcameraOperationWithLease,
|
|
10
|
+
parseSlopcameraOperationInput,
|
|
11
|
+
type SlopcameraOperationDependencies,
|
|
12
|
+
} from "@hraness/slopcamera/operations";
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
import { constants, type BigIntStats, type Stats } from "node:fs";
|
|
15
|
+
import {
|
|
16
|
+
type FileHandle,
|
|
17
|
+
lstat,
|
|
18
|
+
open,
|
|
19
|
+
realpath,
|
|
20
|
+
} from "node:fs/promises";
|
|
21
|
+
import {
|
|
22
|
+
basename,
|
|
23
|
+
dirname,
|
|
24
|
+
isAbsolute,
|
|
25
|
+
join,
|
|
26
|
+
normalize,
|
|
27
|
+
relative,
|
|
28
|
+
resolve,
|
|
29
|
+
sep,
|
|
30
|
+
} from "node:path";
|
|
31
|
+
import { isDeepStrictEqual } from "node:util";
|
|
32
|
+
|
|
33
|
+
import {
|
|
34
|
+
ensurePrivateDirectory,
|
|
35
|
+
ensurePhysicalPrivateDirectoryWithin,
|
|
36
|
+
} from "../../cli/paths";
|
|
37
|
+
import { withMutationLock } from "../../cli/mutation-lock";
|
|
38
|
+
import { sha256Hex } from "../../core/canonical-json";
|
|
39
|
+
import { createNodeBundleFileSystem } from "../../core/storage";
|
|
40
|
+
import type {
|
|
41
|
+
ApplicationContext,
|
|
42
|
+
ApplicationHostResourceLease,
|
|
43
|
+
} from "../context";
|
|
44
|
+
import { ApplicationError } from "../errors";
|
|
45
|
+
import type {
|
|
46
|
+
OperationDefinition,
|
|
47
|
+
OperationExecutionContext,
|
|
48
|
+
OperationLifecycle,
|
|
49
|
+
} from "../operation";
|
|
50
|
+
import { MAXIMUM_DIAGRAM_SOURCE_BYTES } from "./slopcamera-visuals";
|
|
51
|
+
import { bindRepositoryMedia } from "./media/shared";
|
|
52
|
+
|
|
53
|
+
type PortableOperationContract =
|
|
54
|
+
typeof PORTABLE_SLOPCAMERA_OPERATION_CONTRACTS[PortableSlopcameraOperationKind];
|
|
55
|
+
|
|
56
|
+
const portableOperationContracts = Object.freeze(
|
|
57
|
+
PORTABLE_SLOPCAMERA_OPERATION_KINDS.map(
|
|
58
|
+
kind => PORTABLE_SLOPCAMERA_OPERATION_CONTRACTS[kind],
|
|
59
|
+
),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const PORTABLE_OPERATION_COUNT = 4;
|
|
63
|
+
|
|
64
|
+
type PortableExecutor = (
|
|
65
|
+
kind: PortableSlopcameraOperationKind,
|
|
66
|
+
input: unknown,
|
|
67
|
+
dependencies?: SlopcameraOperationDependencies,
|
|
68
|
+
) => Promise<unknown>;
|
|
69
|
+
|
|
70
|
+
type PortableLeasedExecutor = (
|
|
71
|
+
kind: PortableSlopcameraOperationKind,
|
|
72
|
+
input: unknown,
|
|
73
|
+
lease: ApplicationHostResourceLease,
|
|
74
|
+
dependencies?: SlopcameraOperationDependencies,
|
|
75
|
+
) => Promise<unknown>;
|
|
76
|
+
|
|
77
|
+
export interface SlopcameraPortableOperationDependencies {
|
|
78
|
+
readonly execute?: PortableExecutor;
|
|
79
|
+
readonly executeWithLease?: PortableLeasedExecutor;
|
|
80
|
+
readonly parseInput?: (
|
|
81
|
+
kind: PortableSlopcameraOperationKind,
|
|
82
|
+
input: unknown,
|
|
83
|
+
) => unknown;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const NONLOCAL_PATH_PATTERN = /^[A-Za-z][A-Za-z0-9+.-]*:/u;
|
|
87
|
+
|
|
88
|
+
function unsafePath(name: string): never {
|
|
89
|
+
throw new ApplicationError(
|
|
90
|
+
"unsafe-path",
|
|
91
|
+
`${name} must be a bounded local path inside the repository or an explicit absolute caller path.`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function resolvePortablePath(
|
|
96
|
+
repositoryRootValue: string,
|
|
97
|
+
value: unknown,
|
|
98
|
+
name: string,
|
|
99
|
+
): string {
|
|
100
|
+
if (
|
|
101
|
+
typeof value !== "string"
|
|
102
|
+
|| value.length < 1
|
|
103
|
+
|| value.length > 4_096
|
|
104
|
+
|| value.includes("\0")
|
|
105
|
+
|| NONLOCAL_PATH_PATTERN.test(value)
|
|
106
|
+
|| value.startsWith("\\\\")
|
|
107
|
+
|| value.startsWith("//")
|
|
108
|
+
) {
|
|
109
|
+
return unsafePath(name);
|
|
110
|
+
}
|
|
111
|
+
if (isAbsolute(value)) return normalize(value);
|
|
112
|
+
|
|
113
|
+
let repositoryRoot: string;
|
|
114
|
+
let absolutePath: string;
|
|
115
|
+
try {
|
|
116
|
+
repositoryRoot = resolve(repositoryRootValue);
|
|
117
|
+
absolutePath = resolve(repositoryRoot, value);
|
|
118
|
+
} catch {
|
|
119
|
+
return unsafePath(name);
|
|
120
|
+
}
|
|
121
|
+
const repositoryRelativePath = relative(repositoryRoot, absolutePath);
|
|
122
|
+
if (
|
|
123
|
+
repositoryRelativePath === ".."
|
|
124
|
+
|| repositoryRelativePath.startsWith(`..${sep}`)
|
|
125
|
+
|| isAbsolute(repositoryRelativePath)
|
|
126
|
+
) {
|
|
127
|
+
return unsafePath(name);
|
|
128
|
+
}
|
|
129
|
+
return absolutePath;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function inputRecord(value: unknown): Readonly<Record<string, unknown>> {
|
|
133
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
134
|
+
throw new ApplicationError(
|
|
135
|
+
"invalid-data",
|
|
136
|
+
"Portable Slopcamera operation input must be an object.",
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
return value as Readonly<Record<string, unknown>>;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function resolvePortableOperationPaths(
|
|
143
|
+
repositoryRoot: string,
|
|
144
|
+
kind: PortableSlopcameraOperationKind,
|
|
145
|
+
value: unknown,
|
|
146
|
+
): Readonly<Record<string, unknown>> {
|
|
147
|
+
const input = inputRecord(value);
|
|
148
|
+
switch (kind) {
|
|
149
|
+
case "slopcamera.diagram.check":
|
|
150
|
+
return {
|
|
151
|
+
...input,
|
|
152
|
+
path: resolvePortablePath(repositoryRoot, input.path, "path"),
|
|
153
|
+
};
|
|
154
|
+
case "slopcamera.diagram.render":
|
|
155
|
+
return {
|
|
156
|
+
...input,
|
|
157
|
+
path: resolvePortablePath(repositoryRoot, input.path, "path"),
|
|
158
|
+
...(input.outDirectory === undefined
|
|
159
|
+
? {}
|
|
160
|
+
: {
|
|
161
|
+
outDirectory: resolvePortablePath(
|
|
162
|
+
repositoryRoot,
|
|
163
|
+
input.outDirectory,
|
|
164
|
+
"outDirectory",
|
|
165
|
+
),
|
|
166
|
+
}),
|
|
167
|
+
};
|
|
168
|
+
case "slopcamera.image.vectorize":
|
|
169
|
+
return {
|
|
170
|
+
...input,
|
|
171
|
+
inputPath: resolvePortablePath(
|
|
172
|
+
repositoryRoot,
|
|
173
|
+
input.inputPath,
|
|
174
|
+
"inputPath",
|
|
175
|
+
),
|
|
176
|
+
outputPath: resolvePortablePath(
|
|
177
|
+
repositoryRoot,
|
|
178
|
+
input.outputPath,
|
|
179
|
+
"outputPath",
|
|
180
|
+
),
|
|
181
|
+
};
|
|
182
|
+
case "slopcamera.image.generate":
|
|
183
|
+
return {
|
|
184
|
+
...input,
|
|
185
|
+
outputPath: resolvePortablePath(
|
|
186
|
+
repositoryRoot,
|
|
187
|
+
input.outputPath,
|
|
188
|
+
"outputPath",
|
|
189
|
+
),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const PORTABLE_INPUT_SNAPSHOT_ROOT = "portable-operation-inputs/v1";
|
|
195
|
+
const PORTABLE_OUTPUT_LEASE_ROOT = "portable-output-publication-leases";
|
|
196
|
+
const MAXIMUM_VECTOR_SOURCE_BYTES =
|
|
197
|
+
PORTABLE_SLOPCAMERA_OPERATION_CONTRACTS["slopcamera.image.vectorize"]
|
|
198
|
+
.policy.maxInputBytes;
|
|
199
|
+
const portableOutputLeaseTails = new Map<string, Promise<void>>();
|
|
200
|
+
|
|
201
|
+
interface PortableSnapshotIdentity {
|
|
202
|
+
readonly bytes: number;
|
|
203
|
+
readonly device: number;
|
|
204
|
+
readonly inode: number;
|
|
205
|
+
readonly modifiedAtMs: number;
|
|
206
|
+
readonly path: string;
|
|
207
|
+
readonly sha256: string;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
interface PinnedPortableSnapshot {
|
|
211
|
+
readonly assertUnchanged: () => Promise<void>;
|
|
212
|
+
readonly close: () => Promise<void>;
|
|
213
|
+
readonly descriptor: number;
|
|
214
|
+
readonly input: Readonly<Record<string, unknown>>;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function repositoryRelativePath(
|
|
218
|
+
repositoryRootValue: string,
|
|
219
|
+
absolutePathValue: string,
|
|
220
|
+
name: string,
|
|
221
|
+
): string {
|
|
222
|
+
const repositoryRoot = resolve(repositoryRootValue);
|
|
223
|
+
const absolutePath = resolve(absolutePathValue);
|
|
224
|
+
const repositoryPath = relative(repositoryRoot, absolutePath);
|
|
225
|
+
if (
|
|
226
|
+
repositoryPath === ""
|
|
227
|
+
|| repositoryPath === ".."
|
|
228
|
+
|| repositoryPath.startsWith(`..${sep}`)
|
|
229
|
+
|| isAbsolute(repositoryPath)
|
|
230
|
+
) {
|
|
231
|
+
throw new ApplicationError(
|
|
232
|
+
"unsafe-path",
|
|
233
|
+
`${name} must resolve to a physical file inside the repository for durable workflow execution.`,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
return repositoryPath;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function pathWithin(root: string, candidate: string): boolean {
|
|
240
|
+
const fromRoot = relative(root, candidate);
|
|
241
|
+
return fromRoot === ""
|
|
242
|
+
|| (
|
|
243
|
+
fromRoot !== ".."
|
|
244
|
+
&& !fromRoot.startsWith(`..${sep}`)
|
|
245
|
+
&& !isAbsolute(fromRoot)
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function commonPhysicalBundleRoot(
|
|
250
|
+
sourcePath: string,
|
|
251
|
+
privateRoot: string,
|
|
252
|
+
): string {
|
|
253
|
+
let root = dirname(resolve(sourcePath));
|
|
254
|
+
const destination = resolve(privateRoot);
|
|
255
|
+
while (!pathWithin(root, destination)) {
|
|
256
|
+
const parent = dirname(root);
|
|
257
|
+
if (parent === root) {
|
|
258
|
+
throw new ApplicationError(
|
|
259
|
+
"unsafe-path",
|
|
260
|
+
"Portable operation source and private storage do not share a local filesystem root.",
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
root = parent;
|
|
264
|
+
}
|
|
265
|
+
return root;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function noEntry(error: unknown): boolean {
|
|
269
|
+
return error instanceof Error
|
|
270
|
+
&& "code" in error
|
|
271
|
+
&& error.code === "ENOENT";
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function portableOutputTarget(
|
|
275
|
+
kind: PortableSlopcameraOperationKind,
|
|
276
|
+
input: Readonly<Record<string, unknown>>,
|
|
277
|
+
): string | undefined {
|
|
278
|
+
if (kind === "slopcamera.diagram.render") {
|
|
279
|
+
if (typeof input.outDirectory === "string") return input.outDirectory;
|
|
280
|
+
return typeof input.path === "string" ? dirname(input.path) : undefined;
|
|
281
|
+
}
|
|
282
|
+
if (
|
|
283
|
+
kind === "slopcamera.image.generate"
|
|
284
|
+
|| kind === "slopcamera.image.vectorize"
|
|
285
|
+
) {
|
|
286
|
+
return typeof input.outputPath === "string" ? input.outputPath : undefined;
|
|
287
|
+
}
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function canonicalPhysicalOutputTarget(
|
|
292
|
+
targetValue: string,
|
|
293
|
+
directoryTarget: boolean,
|
|
294
|
+
): Promise<string> {
|
|
295
|
+
const target = resolve(targetValue);
|
|
296
|
+
const missing: string[] = [];
|
|
297
|
+
let existing = target;
|
|
298
|
+
let details;
|
|
299
|
+
while (true) {
|
|
300
|
+
try {
|
|
301
|
+
details = await lstat(existing);
|
|
302
|
+
break;
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (!noEntry(error)) throw error;
|
|
305
|
+
const parent = dirname(existing);
|
|
306
|
+
if (parent === existing) throw error;
|
|
307
|
+
missing.unshift(basename(existing));
|
|
308
|
+
existing = parent;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (details.isSymbolicLink() && missing.length === 0 && !directoryTarget) {
|
|
312
|
+
throw new ApplicationError(
|
|
313
|
+
"unsafe-path",
|
|
314
|
+
"Portable operation file outputs must not replace an existing symlink.",
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
const physicalExisting = await realpath(existing);
|
|
318
|
+
const physicalDetails = await lstat(physicalExisting);
|
|
319
|
+
if (
|
|
320
|
+
physicalDetails.isSymbolicLink()
|
|
321
|
+
|| (
|
|
322
|
+
missing.length > 0 || directoryTarget
|
|
323
|
+
? !physicalDetails.isDirectory()
|
|
324
|
+
: !physicalDetails.isFile()
|
|
325
|
+
)
|
|
326
|
+
|| (
|
|
327
|
+
!details.isSymbolicLink()
|
|
328
|
+
&& (
|
|
329
|
+
physicalDetails.dev !== details.dev
|
|
330
|
+
|| physicalDetails.ino !== details.ino
|
|
331
|
+
)
|
|
332
|
+
)
|
|
333
|
+
) {
|
|
334
|
+
throw new ApplicationError(
|
|
335
|
+
"unsafe-path",
|
|
336
|
+
"Portable operation output target ancestry changed during canonicalization.",
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
return resolve(physicalExisting, ...missing);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function canonicalizePortableOutputTarget(
|
|
343
|
+
kind: PortableSlopcameraOperationKind,
|
|
344
|
+
input: Readonly<Record<string, unknown>>,
|
|
345
|
+
repositoryRoot?: string,
|
|
346
|
+
): Promise<Readonly<Record<string, unknown>>> {
|
|
347
|
+
const target = portableOutputTarget(kind, input);
|
|
348
|
+
if (target === undefined) return input;
|
|
349
|
+
const canonical = await canonicalPhysicalOutputTarget(
|
|
350
|
+
target,
|
|
351
|
+
kind === "slopcamera.diagram.render",
|
|
352
|
+
);
|
|
353
|
+
if (
|
|
354
|
+
repositoryRoot !== undefined
|
|
355
|
+
&& !pathWithin(await realpath(repositoryRoot), canonical)
|
|
356
|
+
) {
|
|
357
|
+
throw new ApplicationError(
|
|
358
|
+
"unsafe-path",
|
|
359
|
+
"Repository-relative portable output resolved outside the physical repository root.",
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
return kind === "slopcamera.diagram.render"
|
|
363
|
+
? { ...input, outDirectory: canonical }
|
|
364
|
+
: { ...input, outputPath: canonical };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function portableOutputIsRepositoryRelative(
|
|
368
|
+
kind: PortableSlopcameraOperationKind,
|
|
369
|
+
input: Readonly<Record<string, unknown>>,
|
|
370
|
+
): boolean {
|
|
371
|
+
const target = portableOutputTarget(kind, input);
|
|
372
|
+
return target !== undefined && !isAbsolute(target);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export function slopcameraPortableOutputPublicationParent(
|
|
376
|
+
kind: PortableSlopcameraOperationKind,
|
|
377
|
+
inputValue: unknown,
|
|
378
|
+
): string | undefined {
|
|
379
|
+
const input = inputRecord(inputValue);
|
|
380
|
+
const target = portableOutputTarget(kind, input);
|
|
381
|
+
if (target === undefined) return undefined;
|
|
382
|
+
const parent = kind === "slopcamera.diagram.render"
|
|
383
|
+
? target
|
|
384
|
+
: dirname(target);
|
|
385
|
+
return normalize(parent).normalize("NFC").toLowerCase();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async function portableOutputLeaseDirectory(
|
|
389
|
+
application: ApplicationContext,
|
|
390
|
+
publicationParent: string,
|
|
391
|
+
): Promise<string> {
|
|
392
|
+
let physicalMutationRoot: string;
|
|
393
|
+
if (application.machineStateRoot !== undefined) {
|
|
394
|
+
await ensurePrivateDirectory(application.machineStateRoot);
|
|
395
|
+
physicalMutationRoot = await realpath(application.machineStateRoot);
|
|
396
|
+
} else {
|
|
397
|
+
const repositoryRoot = resolve(application.paths.repositoryRoot);
|
|
398
|
+
const privateRoot = repositoryRelativePath(
|
|
399
|
+
repositoryRoot,
|
|
400
|
+
application.paths.privateRoot,
|
|
401
|
+
"Application private root",
|
|
402
|
+
);
|
|
403
|
+
physicalMutationRoot = await ensurePhysicalPrivateDirectoryWithin(
|
|
404
|
+
repositoryRoot,
|
|
405
|
+
privateRoot,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
const leaseRoot = await ensurePhysicalPrivateDirectoryWithin(
|
|
409
|
+
physicalMutationRoot,
|
|
410
|
+
PORTABLE_OUTPUT_LEASE_ROOT,
|
|
411
|
+
);
|
|
412
|
+
const key = sha256Hex(
|
|
413
|
+
`slopcamera.portable-output-publication-lease/v1\0${publicationParent}`,
|
|
414
|
+
);
|
|
415
|
+
return await ensurePhysicalPrivateDirectoryWithin(leaseRoot, key);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async function withPortableOutputQueue<Value>(
|
|
419
|
+
key: string,
|
|
420
|
+
execute: () => Promise<Value>,
|
|
421
|
+
): Promise<Value> {
|
|
422
|
+
const previous = portableOutputLeaseTails.get(key) ?? Promise.resolve();
|
|
423
|
+
const result = previous.then(execute);
|
|
424
|
+
const settled = result.then(
|
|
425
|
+
() => undefined,
|
|
426
|
+
() => undefined,
|
|
427
|
+
);
|
|
428
|
+
portableOutputLeaseTails.set(key, settled);
|
|
429
|
+
try {
|
|
430
|
+
return await result;
|
|
431
|
+
} finally {
|
|
432
|
+
if (portableOutputLeaseTails.get(key) === settled) {
|
|
433
|
+
portableOutputLeaseTails.delete(key);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function withPortableOutputPublicationLease<Value>(
|
|
439
|
+
application: ApplicationContext,
|
|
440
|
+
publicationParent: string,
|
|
441
|
+
execute: () => Promise<Value>,
|
|
442
|
+
): Promise<Value> {
|
|
443
|
+
const directory = await portableOutputLeaseDirectory(
|
|
444
|
+
application,
|
|
445
|
+
publicationParent,
|
|
446
|
+
);
|
|
447
|
+
return await withPortableOutputQueue(directory, async () =>
|
|
448
|
+
await withMutationLock(directory, {
|
|
449
|
+
command: "workflow:slopcamera-portable",
|
|
450
|
+
label: publicationParent.slice(0, 512),
|
|
451
|
+
now: application.clock.now,
|
|
452
|
+
}, execute));
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async function snapshotPortableSource(
|
|
456
|
+
application: ApplicationContext,
|
|
457
|
+
absoluteSourcePath: string,
|
|
458
|
+
kind: "diagram" | "vector",
|
|
459
|
+
maximumBytes: number,
|
|
460
|
+
signal: AbortSignal,
|
|
461
|
+
): Promise<string> {
|
|
462
|
+
const lexicalRepositoryRoot = resolve(application.paths.repositoryRoot);
|
|
463
|
+
const privateRoot = repositoryRelativePath(
|
|
464
|
+
lexicalRepositoryRoot,
|
|
465
|
+
application.paths.privateRoot,
|
|
466
|
+
"Application private root",
|
|
467
|
+
);
|
|
468
|
+
const physicalPrivateRoot = await ensurePhysicalPrivateDirectoryWithin(
|
|
469
|
+
lexicalRepositoryRoot,
|
|
470
|
+
privateRoot,
|
|
471
|
+
);
|
|
472
|
+
const lexicalSourcePath = resolve(absoluteSourcePath);
|
|
473
|
+
const lexicalRepositorySourcePath = pathWithin(
|
|
474
|
+
lexicalRepositoryRoot,
|
|
475
|
+
lexicalSourcePath,
|
|
476
|
+
)
|
|
477
|
+
? relative(lexicalRepositoryRoot, lexicalSourcePath)
|
|
478
|
+
: undefined;
|
|
479
|
+
const boundRepositorySource = lexicalRepositorySourcePath === undefined
|
|
480
|
+
? undefined
|
|
481
|
+
: await bindRepositoryMedia(
|
|
482
|
+
application,
|
|
483
|
+
{ path: lexicalRepositorySourcePath },
|
|
484
|
+
signal,
|
|
485
|
+
maximumBytes,
|
|
486
|
+
);
|
|
487
|
+
let physicalSource: string;
|
|
488
|
+
if (boundRepositorySource !== undefined) {
|
|
489
|
+
physicalSource = boundRepositorySource.absolutePath;
|
|
490
|
+
} else {
|
|
491
|
+
const lexicalSource = await lstat(lexicalSourcePath);
|
|
492
|
+
if (
|
|
493
|
+
lexicalSource.isSymbolicLink()
|
|
494
|
+
|| !lexicalSource.isFile()
|
|
495
|
+
|| lexicalSource.size < 1
|
|
496
|
+
|| lexicalSource.size > maximumBytes
|
|
497
|
+
) {
|
|
498
|
+
throw new ApplicationError(
|
|
499
|
+
"invalid-data",
|
|
500
|
+
"Portable operation source must be a bounded physical file.",
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
physicalSource = await realpath(lexicalSourcePath);
|
|
504
|
+
const physicalSourceDetails = await lstat(physicalSource);
|
|
505
|
+
if (
|
|
506
|
+
physicalSourceDetails.isSymbolicLink()
|
|
507
|
+
|| !physicalSourceDetails.isFile()
|
|
508
|
+
|| physicalSourceDetails.dev !== lexicalSource.dev
|
|
509
|
+
|| physicalSourceDetails.ino !== lexicalSource.ino
|
|
510
|
+
) {
|
|
511
|
+
throw new ApplicationError(
|
|
512
|
+
"unsafe-path",
|
|
513
|
+
"Portable operation source ancestry changed during physical resolution.",
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
const bundleRoot = commonPhysicalBundleRoot(
|
|
518
|
+
physicalSource,
|
|
519
|
+
physicalPrivateRoot,
|
|
520
|
+
);
|
|
521
|
+
const sourcePath = relative(bundleRoot, physicalSource);
|
|
522
|
+
const fileSystem = createNodeBundleFileSystem(bundleRoot);
|
|
523
|
+
if (
|
|
524
|
+
fileSystem.copyFileNoReplace === undefined
|
|
525
|
+
|| fileSystem.inspectFile === undefined
|
|
526
|
+
) {
|
|
527
|
+
throw new ApplicationError(
|
|
528
|
+
"internal",
|
|
529
|
+
"Portable operation input snapshots require immutable file storage.",
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
const sourceArtifact = boundRepositorySource !== undefined
|
|
533
|
+
? boundRepositorySource.artifact
|
|
534
|
+
: await (async () => {
|
|
535
|
+
const inspected = await fileSystem.inspectFile!(sourcePath);
|
|
536
|
+
if (inspected.bytes < 1 || inspected.bytes > maximumBytes) {
|
|
537
|
+
throw new ApplicationError(
|
|
538
|
+
"invalid-data",
|
|
539
|
+
"Portable operation source exceeds its registered byte limit.",
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
return inspected;
|
|
543
|
+
})();
|
|
544
|
+
const extension = kind === "diagram" ? ".diagram.json" : ".image";
|
|
545
|
+
const absoluteSnapshotPath = resolve(physicalPrivateRoot, join(
|
|
546
|
+
PORTABLE_INPUT_SNAPSHOT_ROOT,
|
|
547
|
+
kind,
|
|
548
|
+
`${sourceArtifact.sha256}${extension}`,
|
|
549
|
+
));
|
|
550
|
+
const snapshotPath = relative(bundleRoot, absoluteSnapshotPath);
|
|
551
|
+
if (sourcePath !== snapshotPath) {
|
|
552
|
+
await fileSystem.copyFileNoReplace(
|
|
553
|
+
sourcePath,
|
|
554
|
+
snapshotPath,
|
|
555
|
+
sourceArtifact,
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
const snapshotHandle = await open(
|
|
559
|
+
absoluteSnapshotPath,
|
|
560
|
+
constants.O_RDONLY | constants.O_NOFOLLOW,
|
|
561
|
+
);
|
|
562
|
+
let snapshotIdentity: Readonly<{ readonly dev: number; readonly ino: number }>;
|
|
563
|
+
try {
|
|
564
|
+
const before = await snapshotHandle.stat();
|
|
565
|
+
if (!before.isFile()) {
|
|
566
|
+
throw new ApplicationError(
|
|
567
|
+
"unsafe-path",
|
|
568
|
+
"Portable operation input snapshot must remain a physical file.",
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
if ((before.mode & 0o777) !== 0o400) {
|
|
572
|
+
await snapshotHandle.chmod(0o400);
|
|
573
|
+
}
|
|
574
|
+
const after = await snapshotHandle.stat();
|
|
575
|
+
if (
|
|
576
|
+
!after.isFile()
|
|
577
|
+
|| after.dev !== before.dev
|
|
578
|
+
|| after.ino !== before.ino
|
|
579
|
+
|| (after.mode & 0o777) !== 0o400
|
|
580
|
+
) {
|
|
581
|
+
throw new ApplicationError(
|
|
582
|
+
"conflict",
|
|
583
|
+
"Portable operation input snapshot changed while it was made read-only.",
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
snapshotIdentity = { dev: after.dev, ino: after.ino };
|
|
587
|
+
} finally {
|
|
588
|
+
await snapshotHandle.close();
|
|
589
|
+
}
|
|
590
|
+
const namedSnapshot = await lstat(absoluteSnapshotPath);
|
|
591
|
+
if (
|
|
592
|
+
namedSnapshot.isSymbolicLink()
|
|
593
|
+
|| !namedSnapshot.isFile()
|
|
594
|
+
|| namedSnapshot.dev !== snapshotIdentity.dev
|
|
595
|
+
|| namedSnapshot.ino !== snapshotIdentity.ino
|
|
596
|
+
|| (namedSnapshot.mode & 0o777) !== 0o400
|
|
597
|
+
) {
|
|
598
|
+
throw new ApplicationError(
|
|
599
|
+
"conflict",
|
|
600
|
+
"Portable operation input snapshot path changed after publication.",
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
const snapshot = await fileSystem.inspectFile(snapshotPath);
|
|
604
|
+
if (
|
|
605
|
+
snapshot.bytes !== sourceArtifact.bytes
|
|
606
|
+
|| snapshot.sha256 !== sourceArtifact.sha256
|
|
607
|
+
) {
|
|
608
|
+
throw new ApplicationError(
|
|
609
|
+
"conflict",
|
|
610
|
+
"Portable operation input snapshot does not match its bound source bytes.",
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
return absoluteSnapshotPath;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
async function assertPortableSnapshotIntegrity(
|
|
617
|
+
application: ApplicationContext,
|
|
618
|
+
kind: PortableSlopcameraOperationKind,
|
|
619
|
+
input: Readonly<Record<string, unknown>>,
|
|
620
|
+
signal: AbortSignal,
|
|
621
|
+
): Promise<PortableSnapshotIdentity | undefined> {
|
|
622
|
+
const source = kind === "slopcamera.diagram.check"
|
|
623
|
+
|| kind === "slopcamera.diagram.render"
|
|
624
|
+
? input.path
|
|
625
|
+
: kind === "slopcamera.image.vectorize"
|
|
626
|
+
? input.inputPath
|
|
627
|
+
: undefined;
|
|
628
|
+
if (typeof source !== "string") return undefined;
|
|
629
|
+
const lexicalRepositoryRoot = resolve(application.paths.repositoryRoot);
|
|
630
|
+
const lexicalPrivateRoot = resolve(application.paths.privateRoot);
|
|
631
|
+
if (
|
|
632
|
+
lexicalPrivateRoot === lexicalRepositoryRoot
|
|
633
|
+
|| !pathWithin(lexicalRepositoryRoot, lexicalPrivateRoot)
|
|
634
|
+
) return undefined;
|
|
635
|
+
const privateRoot = repositoryRelativePath(
|
|
636
|
+
lexicalRepositoryRoot,
|
|
637
|
+
lexicalPrivateRoot,
|
|
638
|
+
"Application private root",
|
|
639
|
+
);
|
|
640
|
+
const physicalRepositoryRoot = await realpath(lexicalRepositoryRoot);
|
|
641
|
+
const stablePhysicalPrivateRoot = resolve(
|
|
642
|
+
physicalRepositoryRoot,
|
|
643
|
+
privateRoot,
|
|
644
|
+
);
|
|
645
|
+
const resolvedSource = resolve(source);
|
|
646
|
+
const stableSourcePath = pathWithin(lexicalRepositoryRoot, resolvedSource)
|
|
647
|
+
? resolve(
|
|
648
|
+
physicalRepositoryRoot,
|
|
649
|
+
relative(lexicalRepositoryRoot, resolvedSource),
|
|
650
|
+
)
|
|
651
|
+
: resolvedSource;
|
|
652
|
+
const snapshotRoot = resolve(
|
|
653
|
+
stablePhysicalPrivateRoot,
|
|
654
|
+
PORTABLE_INPUT_SNAPSHOT_ROOT,
|
|
655
|
+
);
|
|
656
|
+
const snapshotRelativePath = relative(snapshotRoot, stableSourcePath);
|
|
657
|
+
if (
|
|
658
|
+
snapshotRelativePath === ""
|
|
659
|
+
|| snapshotRelativePath === ".."
|
|
660
|
+
|| snapshotRelativePath.startsWith(`..${sep}`)
|
|
661
|
+
|| isAbsolute(snapshotRelativePath)
|
|
662
|
+
) return undefined;
|
|
663
|
+
|
|
664
|
+
let currentPhysicalPrivateRoot: string;
|
|
665
|
+
try {
|
|
666
|
+
currentPhysicalPrivateRoot = await realpath(application.paths.privateRoot);
|
|
667
|
+
} catch {
|
|
668
|
+
throw new ApplicationError(
|
|
669
|
+
"conflict",
|
|
670
|
+
"Portable operation private snapshot storage changed after execution planning.",
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
let currentPrivateRoot: Stats;
|
|
674
|
+
let stablePrivateRoot: Stats;
|
|
675
|
+
try {
|
|
676
|
+
[currentPrivateRoot, stablePrivateRoot] = await Promise.all([
|
|
677
|
+
lstat(application.paths.privateRoot),
|
|
678
|
+
lstat(stablePhysicalPrivateRoot),
|
|
679
|
+
]);
|
|
680
|
+
} catch {
|
|
681
|
+
throw new ApplicationError(
|
|
682
|
+
"conflict",
|
|
683
|
+
"Portable operation private snapshot storage changed after execution planning.",
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
if (
|
|
687
|
+
currentPhysicalPrivateRoot !== stablePhysicalPrivateRoot
|
|
688
|
+
|| currentPrivateRoot.isSymbolicLink()
|
|
689
|
+
|| !currentPrivateRoot.isDirectory()
|
|
690
|
+
|| stablePrivateRoot.isSymbolicLink()
|
|
691
|
+
|| !stablePrivateRoot.isDirectory()
|
|
692
|
+
|| currentPrivateRoot.dev !== stablePrivateRoot.dev
|
|
693
|
+
|| currentPrivateRoot.ino !== stablePrivateRoot.ino
|
|
694
|
+
) {
|
|
695
|
+
throw new ApplicationError(
|
|
696
|
+
"conflict",
|
|
697
|
+
"Portable operation private snapshot storage changed after execution planning.",
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const sourceKind = kind === "slopcamera.image.vectorize"
|
|
702
|
+
? "vector"
|
|
703
|
+
: "diagram";
|
|
704
|
+
const expectedSuffix = sourceKind === "diagram" ? ".diagram.json" : ".image";
|
|
705
|
+
const expectedPrefix = basename(source).slice(0, -expectedSuffix.length);
|
|
706
|
+
const expectedRelativePrefix = `${sourceKind}${sep}`;
|
|
707
|
+
if (
|
|
708
|
+
!snapshotRelativePath.startsWith(expectedRelativePrefix)
|
|
709
|
+
|| snapshotRelativePath.slice(expectedRelativePrefix.length).includes(sep)
|
|
710
|
+
|| !basename(source).endsWith(expectedSuffix)
|
|
711
|
+
|| !/^[a-f0-9]{64}$/u.test(expectedPrefix)
|
|
712
|
+
) {
|
|
713
|
+
throw new ApplicationError(
|
|
714
|
+
"unsafe-path",
|
|
715
|
+
"Portable operation input snapshot has an invalid content-addressed path.",
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
const repositoryPath = repositoryRelativePath(
|
|
719
|
+
physicalRepositoryRoot,
|
|
720
|
+
stableSourcePath,
|
|
721
|
+
"Portable operation input snapshot",
|
|
722
|
+
);
|
|
723
|
+
const before = await lstat(stableSourcePath);
|
|
724
|
+
if (
|
|
725
|
+
before.isSymbolicLink()
|
|
726
|
+
|| !before.isFile()
|
|
727
|
+
|| (before.mode & 0o777) !== 0o400
|
|
728
|
+
) {
|
|
729
|
+
throw new ApplicationError(
|
|
730
|
+
"conflict",
|
|
731
|
+
"Portable operation input snapshot is no longer a read-only physical file.",
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
const inspected = await bindRepositoryMedia(
|
|
735
|
+
application,
|
|
736
|
+
{ path: repositoryPath },
|
|
737
|
+
signal,
|
|
738
|
+
sourceKind === "diagram"
|
|
739
|
+
? MAXIMUM_DIAGRAM_SOURCE_BYTES
|
|
740
|
+
: MAXIMUM_VECTOR_SOURCE_BYTES,
|
|
741
|
+
);
|
|
742
|
+
const after = await lstat(stableSourcePath);
|
|
743
|
+
if (
|
|
744
|
+
inspected.artifact.sha256 !== expectedPrefix
|
|
745
|
+
|| after.isSymbolicLink()
|
|
746
|
+
|| !after.isFile()
|
|
747
|
+
|| after.dev !== before.dev
|
|
748
|
+
|| after.ino !== before.ino
|
|
749
|
+
|| (after.mode & 0o777) !== 0o400
|
|
750
|
+
) {
|
|
751
|
+
throw new ApplicationError(
|
|
752
|
+
"conflict",
|
|
753
|
+
"Portable operation input snapshot changed after execution planning.",
|
|
754
|
+
{
|
|
755
|
+
actualSha256: inspected.artifact.sha256,
|
|
756
|
+
expectedSha256: expectedPrefix,
|
|
757
|
+
path: repositoryPath,
|
|
758
|
+
},
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
return {
|
|
762
|
+
bytes: inspected.artifact.bytes,
|
|
763
|
+
device: inspected.expectedInput.device,
|
|
764
|
+
inode: inspected.expectedInput.inode,
|
|
765
|
+
modifiedAtMs: inspected.expectedInput.modifiedAtMs,
|
|
766
|
+
path: stableSourcePath,
|
|
767
|
+
sha256: expectedPrefix,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function sameExactSnapshot(left: BigIntStats, right: BigIntStats): boolean {
|
|
772
|
+
return left.dev === right.dev
|
|
773
|
+
&& left.ino === right.ino
|
|
774
|
+
&& left.mode === right.mode
|
|
775
|
+
&& left.nlink === right.nlink
|
|
776
|
+
&& left.size === right.size
|
|
777
|
+
&& left.mtimeNs === right.mtimeNs
|
|
778
|
+
&& left.ctimeNs === right.ctimeNs;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
async function hashOpenSnapshot(
|
|
782
|
+
handle: FileHandle,
|
|
783
|
+
bytes: number,
|
|
784
|
+
): Promise<string> {
|
|
785
|
+
const hash = createHash("sha256");
|
|
786
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
787
|
+
let offset = 0;
|
|
788
|
+
while (offset < bytes) {
|
|
789
|
+
const read = await handle.read(
|
|
790
|
+
buffer,
|
|
791
|
+
0,
|
|
792
|
+
Math.min(buffer.byteLength, bytes - offset),
|
|
793
|
+
offset,
|
|
794
|
+
);
|
|
795
|
+
if (read.bytesRead === 0) break;
|
|
796
|
+
hash.update(buffer.subarray(0, read.bytesRead));
|
|
797
|
+
offset += read.bytesRead;
|
|
798
|
+
}
|
|
799
|
+
if (offset !== bytes) {
|
|
800
|
+
throw new ApplicationError(
|
|
801
|
+
"conflict",
|
|
802
|
+
"Portable operation input snapshot ended while its descriptor was pinned.",
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
return hash.digest("hex");
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
async function openPinnedPortableSnapshot(
|
|
809
|
+
application: ApplicationContext,
|
|
810
|
+
kind: PortableSlopcameraOperationKind,
|
|
811
|
+
input: Readonly<Record<string, unknown>>,
|
|
812
|
+
signal: AbortSignal,
|
|
813
|
+
): Promise<PinnedPortableSnapshot | undefined> {
|
|
814
|
+
const expected = await assertPortableSnapshotIntegrity(
|
|
815
|
+
application,
|
|
816
|
+
kind,
|
|
817
|
+
input,
|
|
818
|
+
signal,
|
|
819
|
+
);
|
|
820
|
+
if (expected === undefined) return undefined;
|
|
821
|
+
if (process.platform === "win32") {
|
|
822
|
+
throw new ApplicationError(
|
|
823
|
+
"unavailable",
|
|
824
|
+
"Descriptor-pinned portable operation inputs require a POSIX host.",
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
const handle = await open(
|
|
828
|
+
expected.path,
|
|
829
|
+
constants.O_RDONLY
|
|
830
|
+
| (constants.O_NOFOLLOW ?? 0)
|
|
831
|
+
| (constants.O_NONBLOCK ?? 0),
|
|
832
|
+
);
|
|
833
|
+
try {
|
|
834
|
+
const [opened, exactBefore] = await Promise.all([
|
|
835
|
+
handle.stat(),
|
|
836
|
+
handle.stat({ bigint: true }),
|
|
837
|
+
]);
|
|
838
|
+
if (
|
|
839
|
+
!opened.isFile()
|
|
840
|
+
|| !exactBefore.isFile()
|
|
841
|
+
|| opened.dev !== expected.device
|
|
842
|
+
|| opened.ino !== expected.inode
|
|
843
|
+
|| opened.size !== expected.bytes
|
|
844
|
+
|| opened.mtimeMs !== expected.modifiedAtMs
|
|
845
|
+
|| (opened.mode & 0o777) !== 0o400
|
|
846
|
+
|| await hashOpenSnapshot(handle, expected.bytes) !== expected.sha256
|
|
847
|
+
) {
|
|
848
|
+
throw new ApplicationError(
|
|
849
|
+
"conflict",
|
|
850
|
+
"Portable operation input snapshot changed while its descriptor was pinned.",
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
const descriptorPath = `/dev/fd/${String(handle.fd)}`;
|
|
854
|
+
const processPath = kind === "slopcamera.image.vectorize"
|
|
855
|
+
? "/dev/fd/3"
|
|
856
|
+
: descriptorPath;
|
|
857
|
+
const pinnedInput = kind === "slopcamera.image.vectorize"
|
|
858
|
+
? { ...input, inputPath: processPath }
|
|
859
|
+
: { ...input, path: processPath };
|
|
860
|
+
return {
|
|
861
|
+
assertUnchanged: async () => {
|
|
862
|
+
const [exactAfter, namedAfter, sha256] = await Promise.all([
|
|
863
|
+
handle.stat({ bigint: true }),
|
|
864
|
+
lstat(expected.path),
|
|
865
|
+
hashOpenSnapshot(handle, expected.bytes),
|
|
866
|
+
]);
|
|
867
|
+
if (
|
|
868
|
+
!sameExactSnapshot(exactBefore, exactAfter)
|
|
869
|
+
|| namedAfter.isSymbolicLink()
|
|
870
|
+
|| !namedAfter.isFile()
|
|
871
|
+
|| namedAfter.dev !== expected.device
|
|
872
|
+
|| namedAfter.ino !== expected.inode
|
|
873
|
+
|| (namedAfter.mode & 0o777) !== 0o400
|
|
874
|
+
|| sha256 !== expected.sha256
|
|
875
|
+
) {
|
|
876
|
+
throw new ApplicationError(
|
|
877
|
+
"ambiguous",
|
|
878
|
+
"Portable operation input snapshot changed during execution; output publication is ambiguous.",
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
},
|
|
882
|
+
close: async () => await handle.close(),
|
|
883
|
+
descriptor: handle.fd,
|
|
884
|
+
input: pinnedInput,
|
|
885
|
+
};
|
|
886
|
+
} catch (error) {
|
|
887
|
+
await handle.close();
|
|
888
|
+
throw error;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function restorePlannedDiagramSource(
|
|
893
|
+
kind: PortableSlopcameraOperationKind,
|
|
894
|
+
output: unknown,
|
|
895
|
+
plannedInput: Readonly<Record<string, unknown>>,
|
|
896
|
+
): unknown {
|
|
897
|
+
if (
|
|
898
|
+
kind !== "slopcamera.diagram.render"
|
|
899
|
+
|| typeof output !== "object"
|
|
900
|
+
|| output === null
|
|
901
|
+
|| Array.isArray(output)
|
|
902
|
+
) return output;
|
|
903
|
+
const outputRecord = output as Readonly<Record<string, unknown>>;
|
|
904
|
+
const artifacts = outputRecord.artifacts;
|
|
905
|
+
if (
|
|
906
|
+
typeof artifacts !== "object"
|
|
907
|
+
|| artifacts === null
|
|
908
|
+
|| Array.isArray(artifacts)
|
|
909
|
+
|| typeof plannedInput.path !== "string"
|
|
910
|
+
) return output;
|
|
911
|
+
const artifactRecord = artifacts as Readonly<Record<string, unknown>>;
|
|
912
|
+
return {
|
|
913
|
+
...outputRecord,
|
|
914
|
+
artifacts: { ...artifactRecord, spec: plannedInput.path },
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Resolves portable v2 paths and pins every mutable source to an immutable,
|
|
920
|
+
* content-addressed private file before the scheduler hashes its exact input.
|
|
921
|
+
*/
|
|
922
|
+
export async function bindSlopcameraPortableOperationInputV2(
|
|
923
|
+
application: ApplicationContext,
|
|
924
|
+
kind: PortableSlopcameraOperationKind,
|
|
925
|
+
inputValue: unknown,
|
|
926
|
+
signal: AbortSignal = new AbortController().signal,
|
|
927
|
+
): Promise<unknown> {
|
|
928
|
+
const contract = PORTABLE_SLOPCAMERA_OPERATION_CONTRACTS[kind];
|
|
929
|
+
const input = contract.inputSchema.parse(inputValue);
|
|
930
|
+
const repositoryBoundOutput = portableOutputIsRepositoryRelative(
|
|
931
|
+
kind,
|
|
932
|
+
inputRecord(input),
|
|
933
|
+
);
|
|
934
|
+
const resolved = await canonicalizePortableOutputTarget(
|
|
935
|
+
kind,
|
|
936
|
+
resolvePortableOperationPaths(
|
|
937
|
+
application.paths.repositoryRoot,
|
|
938
|
+
kind,
|
|
939
|
+
input,
|
|
940
|
+
),
|
|
941
|
+
repositoryBoundOutput ? application.paths.repositoryRoot : undefined,
|
|
942
|
+
);
|
|
943
|
+
switch (kind) {
|
|
944
|
+
case "slopcamera.diagram.check":
|
|
945
|
+
return contract.inputSchema.parse({
|
|
946
|
+
...resolved,
|
|
947
|
+
path: await snapshotPortableSource(
|
|
948
|
+
application,
|
|
949
|
+
String(resolved.path),
|
|
950
|
+
"diagram",
|
|
951
|
+
MAXIMUM_DIAGRAM_SOURCE_BYTES,
|
|
952
|
+
signal,
|
|
953
|
+
),
|
|
954
|
+
});
|
|
955
|
+
case "slopcamera.diagram.render": {
|
|
956
|
+
const sourcePath = String(resolved.path);
|
|
957
|
+
return contract.inputSchema.parse({
|
|
958
|
+
...resolved,
|
|
959
|
+
// Once the source is moved into private storage, retain the original
|
|
960
|
+
// default output directory explicitly so rendering semantics do not
|
|
961
|
+
// move with it.
|
|
962
|
+
outDirectory: resolved.outDirectory ?? dirname(sourcePath),
|
|
963
|
+
path: await snapshotPortableSource(
|
|
964
|
+
application,
|
|
965
|
+
sourcePath,
|
|
966
|
+
"diagram",
|
|
967
|
+
MAXIMUM_DIAGRAM_SOURCE_BYTES,
|
|
968
|
+
signal,
|
|
969
|
+
),
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
case "slopcamera.image.vectorize":
|
|
973
|
+
return contract.inputSchema.parse({
|
|
974
|
+
...resolved,
|
|
975
|
+
inputPath: await snapshotPortableSource(
|
|
976
|
+
application,
|
|
977
|
+
String(resolved.inputPath),
|
|
978
|
+
"vector",
|
|
979
|
+
MAXIMUM_VECTOR_SOURCE_BYTES,
|
|
980
|
+
signal,
|
|
981
|
+
),
|
|
982
|
+
});
|
|
983
|
+
case "slopcamera.image.generate":
|
|
984
|
+
return contract.inputSchema.parse(resolved);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function contractDiscovery(contract: PortableOperationContract) {
|
|
989
|
+
return {
|
|
990
|
+
inputSchemaId: contract.inputSchemaId,
|
|
991
|
+
kind: contract.kind,
|
|
992
|
+
lifecycle: contract.lifecycle,
|
|
993
|
+
outputSchemaId: contract.outputSchemaId,
|
|
994
|
+
policy: contract.policy,
|
|
995
|
+
version: contract.version,
|
|
996
|
+
} as const;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function hasImmutablePolicy(value: {
|
|
1000
|
+
readonly policy: {
|
|
1001
|
+
readonly preparation: readonly unknown[];
|
|
1002
|
+
readonly resources: readonly object[];
|
|
1003
|
+
};
|
|
1004
|
+
}): boolean {
|
|
1005
|
+
return Object.isFrozen(value.policy)
|
|
1006
|
+
&& Object.isFrozen(value.policy.preparation)
|
|
1007
|
+
&& Object.isFrozen(value.policy.resources)
|
|
1008
|
+
&& value.policy.resources.every(resource => Object.isFrozen(resource));
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function assertPortableProjectionIntegrity(): void {
|
|
1012
|
+
const contractKinds = Object.keys(PORTABLE_SLOPCAMERA_OPERATION_CONTRACTS)
|
|
1013
|
+
.sort((left, right) => left.localeCompare(right));
|
|
1014
|
+
const portableKinds = [...PORTABLE_SLOPCAMERA_OPERATION_KINDS]
|
|
1015
|
+
.sort((left, right) => left.localeCompare(right));
|
|
1016
|
+
const discoveries = portableOperationContracts
|
|
1017
|
+
.map(contractDiscovery)
|
|
1018
|
+
.sort((left, right) => (
|
|
1019
|
+
left.kind.localeCompare(right.kind) || left.version - right.version
|
|
1020
|
+
));
|
|
1021
|
+
if (
|
|
1022
|
+
PORTABLE_SLOPCAMERA_OPERATION_KINDS.length !== PORTABLE_OPERATION_COUNT
|
|
1023
|
+
|| !isDeepStrictEqual(contractKinds, portableKinds)
|
|
1024
|
+
|| !Object.isFrozen(PORTABLE_SLOPCAMERA_OPERATION_CONTRACTS)
|
|
1025
|
+
|| !Object.isFrozen(PUBLIC_WORKFLOW_REGISTRY_PROJECTION)
|
|
1026
|
+
|| !Object.isFrozen(PUBLIC_WORKFLOW_REGISTRY_PROJECTION.discovery)
|
|
1027
|
+
|| portableOperationContracts.some(contract => (
|
|
1028
|
+
!Object.isFrozen(contract)
|
|
1029
|
+
|| !hasImmutablePolicy(contract)
|
|
1030
|
+
))
|
|
1031
|
+
|| PUBLIC_WORKFLOW_REGISTRY_PROJECTION.discovery.some(operation => (
|
|
1032
|
+
!Object.isFrozen(operation)
|
|
1033
|
+
|| !hasImmutablePolicy(operation)
|
|
1034
|
+
))
|
|
1035
|
+
|| !isDeepStrictEqual(
|
|
1036
|
+
discoveries,
|
|
1037
|
+
PUBLIC_WORKFLOW_REGISTRY_PROJECTION.discovery,
|
|
1038
|
+
)
|
|
1039
|
+
) {
|
|
1040
|
+
throw new ApplicationError(
|
|
1041
|
+
"incompatible",
|
|
1042
|
+
"The shared portable Slopcamera contracts drifted from their public workflow projection.",
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
async function executePortableOperation(
|
|
1048
|
+
context: OperationExecutionContext,
|
|
1049
|
+
contract: PortableOperationContract,
|
|
1050
|
+
input: unknown,
|
|
1051
|
+
dependencies: SlopcameraPortableOperationDependencies,
|
|
1052
|
+
): Promise<unknown> {
|
|
1053
|
+
const schemaInput = contract.inputSchema.parse(input);
|
|
1054
|
+
const pathResolvedInput = resolvePortableOperationPaths(
|
|
1055
|
+
context.application.paths.repositoryRoot,
|
|
1056
|
+
contract.kind,
|
|
1057
|
+
schemaInput,
|
|
1058
|
+
);
|
|
1059
|
+
const resolvedInput = await canonicalizePortableOutputTarget(
|
|
1060
|
+
contract.kind,
|
|
1061
|
+
pathResolvedInput,
|
|
1062
|
+
portableOutputIsRepositoryRelative(
|
|
1063
|
+
contract.kind,
|
|
1064
|
+
inputRecord(schemaInput),
|
|
1065
|
+
)
|
|
1066
|
+
? context.application.paths.repositoryRoot
|
|
1067
|
+
: undefined,
|
|
1068
|
+
);
|
|
1069
|
+
if (
|
|
1070
|
+
portableOutputTarget(contract.kind, pathResolvedInput)
|
|
1071
|
+
!== portableOutputTarget(contract.kind, resolvedInput)
|
|
1072
|
+
) {
|
|
1073
|
+
throw new ApplicationError(
|
|
1074
|
+
"conflict",
|
|
1075
|
+
"Portable operation output target is no longer its canonical execution-planned path.",
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
const execute = async (): Promise<unknown> => {
|
|
1079
|
+
const revalidatedInput = await canonicalizePortableOutputTarget(
|
|
1080
|
+
contract.kind,
|
|
1081
|
+
resolvedInput,
|
|
1082
|
+
);
|
|
1083
|
+
if (
|
|
1084
|
+
portableOutputTarget(contract.kind, revalidatedInput)
|
|
1085
|
+
!== portableOutputTarget(contract.kind, resolvedInput)
|
|
1086
|
+
) {
|
|
1087
|
+
throw new ApplicationError(
|
|
1088
|
+
"conflict",
|
|
1089
|
+
"Portable operation output target changed before dispatch.",
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
const pinned = await openPinnedPortableSnapshot(
|
|
1093
|
+
context.application,
|
|
1094
|
+
contract.kind,
|
|
1095
|
+
resolvedInput,
|
|
1096
|
+
context.abortSignal,
|
|
1097
|
+
);
|
|
1098
|
+
try {
|
|
1099
|
+
const executionInput = pinned?.input ?? resolvedInput;
|
|
1100
|
+
const parsedInput = (dependencies.parseInput ?? parseSlopcameraOperationInput)(
|
|
1101
|
+
contract.kind,
|
|
1102
|
+
executionInput,
|
|
1103
|
+
);
|
|
1104
|
+
const inheritedFileDescriptors = [
|
|
1105
|
+
...(pinned === undefined ? [] : [pinned.descriptor]),
|
|
1106
|
+
...(context.application.hostResourceLease?.inheritedFileDescriptors ?? []),
|
|
1107
|
+
].filter((descriptor, index, descriptors) => (
|
|
1108
|
+
descriptors.indexOf(descriptor) === index
|
|
1109
|
+
));
|
|
1110
|
+
const executionDependencies: SlopcameraOperationDependencies = {
|
|
1111
|
+
signal: context.abortSignal,
|
|
1112
|
+
...(inheritedFileDescriptors.length === 0
|
|
1113
|
+
? {}
|
|
1114
|
+
: { inheritedFileDescriptors }),
|
|
1115
|
+
};
|
|
1116
|
+
const output = context.application.hostResourceLease === undefined
|
|
1117
|
+
? await (dependencies.execute ?? executeSlopcameraOperation)(
|
|
1118
|
+
contract.kind,
|
|
1119
|
+
parsedInput,
|
|
1120
|
+
executionDependencies,
|
|
1121
|
+
)
|
|
1122
|
+
: await (dependencies.executeWithLease ?? executeSlopcameraOperationWithLease)(
|
|
1123
|
+
contract.kind,
|
|
1124
|
+
parsedInput,
|
|
1125
|
+
context.application.hostResourceLease,
|
|
1126
|
+
executionDependencies,
|
|
1127
|
+
);
|
|
1128
|
+
await pinned?.assertUnchanged();
|
|
1129
|
+
return contract.outputSchema.parse(restorePlannedDiagramSource(
|
|
1130
|
+
contract.kind,
|
|
1131
|
+
output,
|
|
1132
|
+
resolvedInput,
|
|
1133
|
+
));
|
|
1134
|
+
} finally {
|
|
1135
|
+
await pinned?.close();
|
|
1136
|
+
}
|
|
1137
|
+
};
|
|
1138
|
+
const publicationParent = slopcameraPortableOutputPublicationParent(
|
|
1139
|
+
contract.kind,
|
|
1140
|
+
resolvedInput,
|
|
1141
|
+
);
|
|
1142
|
+
return publicationParent === undefined
|
|
1143
|
+
? await execute()
|
|
1144
|
+
: await withPortableOutputPublicationLease(
|
|
1145
|
+
context.application,
|
|
1146
|
+
publicationParent,
|
|
1147
|
+
execute,
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function createPortableLifecycle(
|
|
1152
|
+
contract: PortableOperationContract,
|
|
1153
|
+
dependencies: SlopcameraPortableOperationDependencies,
|
|
1154
|
+
): OperationLifecycle<unknown, unknown> {
|
|
1155
|
+
const execute = async (
|
|
1156
|
+
context: OperationExecutionContext,
|
|
1157
|
+
input: unknown,
|
|
1158
|
+
): Promise<unknown> => await executePortableOperation(
|
|
1159
|
+
context,
|
|
1160
|
+
contract,
|
|
1161
|
+
input,
|
|
1162
|
+
dependencies,
|
|
1163
|
+
);
|
|
1164
|
+
return {
|
|
1165
|
+
execute,
|
|
1166
|
+
kind: contract.lifecycle,
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
export function createSlopcameraPortableOperationDefinitions(
|
|
1171
|
+
dependencies: SlopcameraPortableOperationDependencies = {},
|
|
1172
|
+
): readonly OperationDefinition[] {
|
|
1173
|
+
assertPortableProjectionIntegrity();
|
|
1174
|
+
return Object.freeze(portableOperationContracts.map(contract => ({
|
|
1175
|
+
inputSchema: contract.inputSchema,
|
|
1176
|
+
inputSchemaId: contract.inputSchemaId,
|
|
1177
|
+
kind: contract.kind,
|
|
1178
|
+
lifecycle: createPortableLifecycle(contract, dependencies),
|
|
1179
|
+
outputSchema: contract.outputSchema,
|
|
1180
|
+
outputSchemaId: contract.outputSchemaId,
|
|
1181
|
+
policy: contract.policy,
|
|
1182
|
+
summarize: () => ({ fields: {}, kind: contract.kind }),
|
|
1183
|
+
version: contract.version,
|
|
1184
|
+
})));
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
export const slopcameraPortableOperationDefinitions =
|
|
1188
|
+
createSlopcameraPortableOperationDefinitions();
|