@volter/editor-core 0.5.57
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/BUNDLED_NOTICES +1527 -0
- package/LICENSE +686 -0
- package/LICENSE-APACHE +202 -0
- package/NOTICE +18 -0
- package/README.md +24 -0
- package/dist/build/chunk-JS4D4XZ3.js +175 -0
- package/dist/build/chunk-JS4D4XZ3.js.map +7 -0
- package/dist/build/vite-plugin-product-contributions.js +100 -0
- package/dist/build/vite-plugin-product-contributions.js.map +7 -0
- package/dist/build/vite-plugin-shared-react.js +25 -0
- package/dist/build/vite-plugin-shared-react.js.map +7 -0
- package/dist/build/vite-plugin-shared-three.js +86 -0
- package/dist/build/vite-plugin-shared-three.js.map +7 -0
- package/dist/server/chunk-KMJC7AGO.js +98 -0
- package/dist/server/chunk-KMJC7AGO.js.map +7 -0
- package/dist/server/chunk-PSILUMGS.js +46 -0
- package/dist/server/chunk-PSILUMGS.js.map +7 -0
- package/dist/server/frame-proxy.js +255 -0
- package/dist/server/frame-proxy.js.map +7 -0
- package/dist/server/open-browser.js +53 -0
- package/dist/server/open-browser.js.map +7 -0
- package/dist/server/process-shutdown.js +127 -0
- package/dist/server/process-shutdown.js.map +7 -0
- package/dist/server/session-registry.js +141 -0
- package/dist/server/session-registry.js.map +7 -0
- package/dist/server/spawn-opener.js +11 -0
- package/dist/server/spawn-opener.js.map +7 -0
- package/dist/server/worktree-identity.js +11 -0
- package/dist/server/worktree-identity.js.map +7 -0
- package/dist-server/packaged.mjs +88816 -0
- package/package.json +118 -0
- package/scripts/build-plugins.mjs +14 -0
- package/scripts/build-server.mjs +26 -0
- package/scripts/build-session.mjs +9 -0
- package/server/account-credentials.ts +60 -0
- package/server/account-service.ts +1434 -0
- package/server/adapter-region-includes.ts +47 -0
- package/server/asset-catalog-v2.ts +175 -0
- package/server/asset-history-snapshots.ts +196 -0
- package/server/asset-ledger-store.ts +195 -0
- package/server/asset-library-routes.ts +1595 -0
- package/server/blender-wasm-artifact.ts +430 -0
- package/server/boot-timings.ts +106 -0
- package/server/canonical-path.ts +50 -0
- package/server/checkout-workspace-preflight.ts +349 -0
- package/server/cloud-asset-catalog.ts +158 -0
- package/server/coding-inference-launch.ts +237 -0
- package/server/collaboration-account-client.ts +202 -0
- package/server/collaboration-attribution.ts +122 -0
- package/server/collaboration-session.ts +934 -0
- package/server/comfyui-bridge.ts +248 -0
- package/server/console-ledger.ts +499 -0
- package/server/creation-site-transform.ts +304 -0
- package/server/creation-site-write.ts +648 -0
- package/server/data-file-serialize.ts +158 -0
- package/server/editor-brand-html.ts +72 -0
- package/server/editor-control-socket.ts +366 -0
- package/server/editor-server-options.ts +190 -0
- package/server/editor-server.ts +1483 -0
- package/server/editor-sse.ts +352 -0
- package/server/engine-provenance.ts +169 -0
- package/server/engine-source-restart.ts +117 -0
- package/server/frame-bridge.ts +115 -0
- package/server/frame-proxy.ts +490 -0
- package/server/frame-workbench.ts +259 -0
- package/server/frontend-handoff.ts +229 -0
- package/server/game-globals-shadow.ts +113 -0
- package/server/gameplay-session-retention.ts +149 -0
- package/server/gameplay-sessions.ts +123 -0
- package/server/generation-jobs.ts +201 -0
- package/server/generation-reconciler.ts +137 -0
- package/server/generative-execution-context.ts +59 -0
- package/server/git-workflow.ts +612 -0
- package/server/harness-chat-caller.ts +36 -0
- package/server/harness-chat-service.ts +1681 -0
- package/server/idle-shutdown.ts +170 -0
- package/server/js-profiling-policy.ts +52 -0
- package/server/launcher-settings.ts +84 -0
- package/server/local-asset-catalog.ts +410 -0
- package/server/managed-account-defaults.ts +22 -0
- package/server/mock-control-plane.ts +540 -0
- package/server/model-import-conversion.ts +345 -0
- package/server/native-credential-store.ts +162 -0
- package/server/open-browser.ts +118 -0
- package/server/packaged.ts +1192 -0
- package/server/play-stall.ts +126 -0
- package/server/process-shutdown.ts +218 -0
- package/server/product-presets.ts +55 -0
- package/server/project-build-artifact.ts +95 -0
- package/server/project-components.ts +100 -0
- package/server/project-dependency-invalidation.ts +114 -0
- package/server/project-file-scan.ts +235 -0
- package/server/project-hmr-files.ts +215 -0
- package/server/project-install-roots.ts +210 -0
- package/server/project-kinds.ts +71 -0
- package/server/project-mcp-servers.ts +82 -0
- package/server/project-module-freshness.ts +109 -0
- package/server/project-module-instance.ts +375 -0
- package/server/project-optimize-deps-entries.ts +827 -0
- package/server/project-output-writer.ts +679 -0
- package/server/project-package-origin.ts +63 -0
- package/server/project-root-surface.ts +831 -0
- package/server/project-scratch-path.ts +47 -0
- package/server/project-script-hmr.ts +619 -0
- package/server/project-serving-plugins.ts +236 -0
- package/server/project-tools.ts +653 -0
- package/server/project-validation.ts +250 -0
- package/server/project-verbs.ts +127 -0
- package/server/project-view.ts +52 -0
- package/server/project-watch.ts +1134 -0
- package/server/project-work-coordinator.ts +288 -0
- package/server/provider-credentials.ts +331 -0
- package/server/recent-projects-store.ts +98 -0
- package/server/redact-secrets.ts +71 -0
- package/server/repository-presence.ts +267 -0
- package/server/routes/account.ts +357 -0
- package/server/routes/agents.ts +60 -0
- package/server/routes/assets.ts +154 -0
- package/server/routes/build.ts +13 -0
- package/server/routes/collaboration.ts +595 -0
- package/server/routes/configurations.ts +419 -0
- package/server/routes/context.ts +264 -0
- package/server/routes/control-plane.ts +1811 -0
- package/server/routes/logs.ts +272 -0
- package/server/routes/project-identity.ts +302 -0
- package/server/routes/project-open.ts +429 -0
- package/server/routes/project-source.ts +686 -0
- package/server/routes/project-state.ts +383 -0
- package/server/routes/relay.ts +1029 -0
- package/server/routes/route-helpers.ts +29 -0
- package/server/routes/served-modules.ts +138 -0
- package/server/routes/session-tabs.ts +196 -0
- package/server/routes/settings.ts +121 -0
- package/server/routes/share-control.ts +486 -0
- package/server/routes/themes.ts +116 -0
- package/server/routes/tools.ts +230 -0
- package/server/routes/worktrees.ts +595 -0
- package/server/scoped-game-css.ts +461 -0
- package/server/server-utils.ts +1194 -0
- package/server/session-product.ts +112 -0
- package/server/session-registry.ts +230 -0
- package/server/share-claims.ts +138 -0
- package/server/share-host.ts +526 -0
- package/server/share-session-gateway.ts +1362 -0
- package/server/share-tunnel.ts +123 -0
- package/server/spawn-opener.ts +74 -0
- package/server/support/play/log-format.ts +76 -0
- package/server/support/play/session-record.ts +199 -0
- package/server/support/project/build-discipline.ts +718 -0
- package/server/support/project/inspection-node.ts +119 -0
- package/server/support/project/log-naming.ts +54 -0
- package/server/support/project/provenance.ts +290 -0
- package/server/support/project/run-name.ts +18 -0
- package/server/support/project/session-journal.ts +830 -0
- package/server/support/project/shared.ts +327 -0
- package/server/tab-bootstrap.ts +62 -0
- package/server/tab-heartbeat.ts +477 -0
- package/server/tab-lifecycle.ts +550 -0
- package/server/tab-presence.ts +1508 -0
- package/server/team-agent-mirror.ts +95 -0
- package/server/twin-auth.ts +138 -0
- package/server/vendored-lock-recorder.ts +968 -0
- package/server/worktree-identity.ts +121 -0
- package/server/worktree-management.ts +398 -0
- package/server/worktree-retention.d.mts +13 -0
- package/server/worktree-retention.mjs +205 -0
- package/src/EditorContext.tsx +169 -0
- package/src/account.ts +333 -0
- package/src/action-registry.ts +532 -0
- package/src/active-product.ts +74 -0
- package/src/active-project.ts +155 -0
- package/src/adapter-editor-config.ts +25 -0
- package/src/adapter-observation.ts +49 -0
- package/src/animation/stage-transport.ts +379 -0
- package/src/animation/three-clips-subject.ts +172 -0
- package/src/api/asset-library-wire.ts +45 -0
- package/src/api/assets.ts +365 -0
- package/src/api/base.ts +10 -0
- package/src/api/build.ts +99 -0
- package/src/api/git-wire.ts +56 -0
- package/src/api/logs.ts +92 -0
- package/src/api/project-identity.ts +74 -0
- package/src/api/project-open.ts +355 -0
- package/src/api/project-source.ts +162 -0
- package/src/api/project-state.ts +107 -0
- package/src/api/relay.ts +268 -0
- package/src/api/settings.ts +36 -0
- package/src/api/themes.ts +45 -0
- package/src/api/worktrees.ts +257 -0
- package/src/asset-compare-core.ts +171 -0
- package/src/asset-compare.ts +294 -0
- package/src/asset-editor-context.tsx +100 -0
- package/src/asset-events.ts +97 -0
- package/src/asset-inspector-actions.ts +87 -0
- package/src/asset-preview-framing.ts +357 -0
- package/src/asset-preview.ts +2802 -0
- package/src/asset-selection-viewer-registry.ts +113 -0
- package/src/asset-selection.ts +133 -0
- package/src/asset-workflow/asset-capabilities.ts +344 -0
- package/src/asset-workflow/asset-import-jobs.ts +106 -0
- package/src/asset-workflow/asset-ledger-backend.ts +126 -0
- package/src/asset-workflow/asset-ledger.ts +156 -0
- package/src/asset-workflow/asset-materialization-report.ts +140 -0
- package/src/asset-workflow/asset-pack-manifest.ts +320 -0
- package/src/asset-workflow/asset-types.ts +142 -0
- package/src/asset-workflow/asset-workflow-quality.ts +61 -0
- package/src/asset-workflow/audio-preview-player.ts +193 -0
- package/src/asset-workflow/audio-waveform.ts +22 -0
- package/src/asset-workflow/cloud-asset-client.ts +263 -0
- package/src/asset-workflow/folder-preview.ts +345 -0
- package/src/asset-workflow/hosted-asset-materialization.ts +236 -0
- package/src/asset-workflow/image-view-scale.ts +32 -0
- package/src/asset-workflow/import-contract.ts +124 -0
- package/src/asset-workflow/ledger-write-lock.ts +244 -0
- package/src/asset-workflow/model-inspection.ts +818 -0
- package/src/asset-workflow/pixi-spritesheet.ts +197 -0
- package/src/asset-workflow/preview-resource-lifetime.ts +44 -0
- package/src/asset-workflow/project-asset-commands.ts +23 -0
- package/src/asset-workflow/project-asset-health.ts +139 -0
- package/src/asset-workflow/project-asset-operations.ts +154 -0
- package/src/asset-workflow/project-asset-roots.ts +68 -0
- package/src/asset-workflow/project-content.ts +288 -0
- package/src/asset-workflow/project-source-index.ts +550 -0
- package/src/asset-workflow/thumbnail-system.ts +256 -0
- package/src/authoring/active-adapter.ts +199 -0
- package/src/authoring/active-systems.ts +422 -0
- package/src/authoring/adapter-key.ts +18 -0
- package/src/authoring/authoring-asset-url.ts +27 -0
- package/src/authoring/bootstrap-state.ts +49 -0
- package/src/authoring/boundary-authoring-adapter.ts +184 -0
- package/src/authoring/breakpoint-state.ts +43 -0
- package/src/authoring/canvas-scene-guides.ts +76 -0
- package/src/authoring/component-instance-root.ts +171 -0
- package/src/authoring/composite-authoring-adapter.ts +2110 -0
- package/src/authoring/consumer-actions.ts +520 -0
- package/src/authoring/css-numeric-style.ts +97 -0
- package/src/authoring/design-time-layers.ts +849 -0
- package/src/authoring/design-time-mount-registry.ts +235 -0
- package/src/authoring/design-time-settle.ts +343 -0
- package/src/authoring/edit-mode-authoring.ts +619 -0
- package/src/authoring/eyedropper-session.ts +60 -0
- package/src/authoring/instance-source-menu-register.ts +13 -0
- package/src/authoring/instance-source-menu.ts +135 -0
- package/src/authoring/layered-pick.ts +183 -0
- package/src/authoring/live-gesture-lock.ts +50 -0
- package/src/authoring/live-object-transform.ts +62 -0
- package/src/authoring/mount-failure-report.ts +154 -0
- package/src/authoring/mounted-root-subjects.ts +144 -0
- package/src/authoring/no-authoring-adapter.ts +55 -0
- package/src/authoring/null-inspection-subjects.tsx +75 -0
- package/src/authoring/object3d-document-persistence.ts +106 -0
- package/src/authoring/object3d-document-session-registry.ts +120 -0
- package/src/authoring/object3d-document-session.ts +1326 -0
- package/src/authoring/object3d-gesture-controller.ts +113 -0
- package/src/authoring/panel-authoring.ts +121 -0
- package/src/authoring/prefab-instance-inspector-section.tsx +237 -0
- package/src/authoring/prefab-instance-section-model.ts +55 -0
- package/src/authoring/project-authoring-session.ts +105 -0
- package/src/authoring/provenance.ts +99 -0
- package/src/authoring/quarks-particle-systems.ts +19 -0
- package/src/authoring/react-canvas-navigation.ts +255 -0
- package/src/authoring/react-design-canvas-style.ts +20 -0
- package/src/authoring/react-story-board.ts +937 -0
- package/src/authoring/selection-scope.ts +195 -0
- package/src/authoring/shell-document-ops.ts +169 -0
- package/src/authoring/shell-object3d-document-write-policy.ts +108 -0
- package/src/authoring/shell-viewport-policy.ts +48 -0
- package/src/authoring/source-object3d-authoring-adapter.ts +526 -0
- package/src/authoring/stories-scope.ts +35 -0
- package/src/authoring/story-board-chrome-fit.ts +107 -0
- package/src/authoring/story-board-presentation.ts +111 -0
- package/src/authoring/three-projection-core.ts +226 -0
- package/src/authoring/viewport-pick-context.ts +40 -0
- package/src/authoring/viewport-raycast.ts +240 -0
- package/src/authoring/viewport-tool-context.ts +73 -0
- package/src/authoring/world-canvas-viewport-state.ts +35 -0
- package/src/authoring/world-hidden-viewport.ts +152 -0
- package/src/authoring/world-pan-state.ts +198 -0
- package/src/authoring/world-session-state.ts +101 -0
- package/src/authoring/write-pipe.ts +173 -0
- package/src/availability-tick.ts +66 -0
- package/src/bitmap-label.ts +120 -0
- package/src/blender-tab-metrics.ts +161 -0
- package/src/board-open-actions.ts +20 -0
- package/src/boot-routing.ts +391 -0
- package/src/build-identity.ts +15 -0
- package/src/bytes-codec.ts +62 -0
- package/src/camera-authoring.ts +175 -0
- package/src/cancellation-reason.ts +58 -0
- package/src/canvas-preview-frames.ts +482 -0
- package/src/capture-camera-pose.ts +77 -0
- package/src/chrome-registry.ts +159 -0
- package/src/chrome-slot-registry.ts +91 -0
- package/src/collaboration-client.ts +264 -0
- package/src/collaboration-presence.ts +41 -0
- package/src/command-dispatch.ts +19 -0
- package/src/command-listener.ts +2743 -0
- package/src/command-registry.ts +70 -0
- package/src/component-board-registry.ts +205 -0
- package/src/component-states-registry.ts +200 -0
- package/src/components/AgentPresentationNotice.tsx +75 -0
- package/src/components/AlignToolbar.tsx +204 -0
- package/src/components/AppRoot.tsx +394 -0
- package/src/components/ApplicationMenus.tsx +415 -0
- package/src/components/AssetBrowser.tsx +2843 -0
- package/src/components/AssetEditorShell.tsx +216 -0
- package/src/components/AssetInspectorToolSection.tsx +124 -0
- package/src/components/BoardRulers.tsx +354 -0
- package/src/components/CameraInfo.tsx +81 -0
- package/src/components/CanvasSceneViewport.tsx +556 -0
- package/src/components/CapabilityCoverageSection.tsx +185 -0
- package/src/components/CenterDocuments.tsx +22 -0
- package/src/components/ChromeSlot.tsx +20 -0
- package/src/components/CodeView.tsx +470 -0
- package/src/components/CollaborationHeaderControl.css +74 -0
- package/src/components/CompactInspectorCard.tsx +190 -0
- package/src/components/CompactInspectorShell.tsx +39 -0
- package/src/components/ConsolePanel.css +55 -0
- package/src/components/ConsolePanel.tsx +270 -0
- package/src/components/DefaultEditorLayout.tsx +106 -0
- package/src/components/DocumentHeaderStrip.tsx +96 -0
- package/src/components/DocumentShelfRail.tsx +45 -0
- package/src/components/DocumentThumbnail.tsx +345 -0
- package/src/components/EditorLeaseGuard.tsx +372 -0
- package/src/components/ErrorBanner.tsx +123 -0
- package/src/components/FolderPreviewTile.tsx +159 -0
- package/src/components/GameHierarchy.tsx +3842 -0
- package/src/components/GameplaySessionTimeline.tsx +295 -0
- package/src/components/InspectionProjection.tsx +968 -0
- package/src/components/Inspector.tsx +233 -0
- package/src/components/InspectorCanvasPreview.tsx +35 -0
- package/src/components/InspectorFieldsSection.tsx +290 -0
- package/src/components/InspectorObjectPreview.tsx +57 -0
- package/src/components/InspectorStoriesSection.tsx +92 -0
- package/src/components/InspectorToolSection.tsx +96 -0
- package/src/components/InspectorTransformSection.tsx +245 -0
- package/src/components/LightExplorerPanel.tsx +433 -0
- package/src/components/MediaProperties.tsx +145 -0
- package/src/components/NonThreeAuthoringBootstrap.tsx +19 -0
- package/src/components/Object3DDocumentToolbar.css +100 -0
- package/src/components/Object3DDocumentToolbar.tsx +454 -0
- package/src/components/Object3DDocumentViewport.tsx +58 -0
- package/src/components/ProjectHeader.css +617 -0
- package/src/components/ProjectHeader.tsx +328 -0
- package/src/components/ProjectLayout.tsx +249 -0
- package/src/components/ReactCanvasControls.tsx +284 -0
- package/src/components/RootSelectionOverlay.tsx +3162 -0
- package/src/components/RootTextEditor.tsx +79 -0
- package/src/components/SaveStatus.tsx +70 -0
- package/src/components/StageHost.tsx +1954 -0
- package/src/components/StageOverlays.tsx +21 -0
- package/src/components/StartupErrorScreen.tsx +76 -0
- package/src/components/StartupLoadingScreen.tsx +65 -0
- package/src/components/StatsOverlay.tsx +78 -0
- package/src/components/SurfaceStateOverlay.tsx +24 -0
- package/src/components/ToolContributionSurfaces.tsx +65 -0
- package/src/components/ToolHost.tsx +370 -0
- package/src/components/ToolObject3DPreview.tsx +39 -0
- package/src/components/Toolbar.tsx +595 -0
- package/src/components/TransientHint.tsx +44 -0
- package/src/components/TransportStrip.tsx +174 -0
- package/src/components/VersionControlSection.tsx +470 -0
- package/src/components/VgaiLogo.css +83 -0
- package/src/components/VgaiLogo.tsx +35 -0
- package/src/components/ViewportControlsHint.tsx +60 -0
- package/src/components/ViewportFurniture.tsx +401 -0
- package/src/components/ViewportOverlay.tsx +145 -0
- package/src/components/ViewportOverlaysMenu.tsx +153 -0
- package/src/components/ViewportShadingMenu.tsx +300 -0
- package/src/components/ViewportViewMenu.tsx +101 -0
- package/src/components/WorkspaceDocumentSurface.tsx +204 -0
- package/src/components/WorkspaceUtilitySurface.tsx +22 -0
- package/src/components/WorktreeSwitcher.css +239 -0
- package/src/components/WorktreeSwitcher.tsx +890 -0
- package/src/components/account-documents.tsx +1162 -0
- package/src/components/asset-documents.tsx +812 -0
- package/src/components/asset-editor-persistence.ts +216 -0
- package/src/components/asset-selection-section.tsx +535 -0
- package/src/components/asset-thumbnails.tsx +312 -0
- package/src/components/asset-viewers/AudioViewer.tsx +201 -0
- package/src/components/asset-viewers/EntityModelDocument.tsx +122 -0
- package/src/components/asset-viewers/EnvironmentAssetDocument.tsx +440 -0
- package/src/components/asset-viewers/GenericJsonViewer.tsx +102 -0
- package/src/components/asset-viewers/ImageViewer.tsx +300 -0
- package/src/components/asset-viewers/JsonAssetDocument.tsx +93 -0
- package/src/components/asset-viewers/LiveModuleDocument.tsx +417 -0
- package/src/components/asset-viewers/LutAssetDocument.tsx +444 -0
- package/src/components/asset-viewers/ModelAssetDocument.tsx +105 -0
- package/src/components/asset-viewers/Object3DPreview.tsx +359 -0
- package/src/components/asset-viewers/OnlineAssetDetail.tsx +421 -0
- package/src/components/asset-viewers/PasteboardModuleDocument.tsx +67 -0
- package/src/components/asset-viewers/QuarksAssetDocument.tsx +526 -0
- package/src/components/asset-viewers/ShaderAssetDocument.tsx +743 -0
- package/src/components/asset-viewers/SourceAssetViewer.tsx +281 -0
- package/src/components/asset-viewers/SpritesheetSpriteView.tsx +102 -0
- package/src/components/asset-viewers/VideoViewer.tsx +101 -0
- package/src/components/asset-viewers/shader-source.ts +144 -0
- package/src/components/asset-workflow.css +731 -0
- package/src/components/board-guides.ts +150 -0
- package/src/components/compact-inspector.css +539 -0
- package/src/components/core-utilities.tsx +90 -0
- package/src/components/editor-notifications.css +40 -0
- package/src/components/engine-workspace.css +274 -0
- package/src/components/inspector-preview-section.tsx +238 -0
- package/src/components/inspector-property-grouping.ts +64 -0
- package/src/components/inspector-revert-label.ts +20 -0
- package/src/components/inspector-selection.ts +42 -0
- package/src/components/inspector-stories-gating.ts +171 -0
- package/src/components/inspector-transform-subject.ts +11 -0
- package/src/components/inspector-transform.ts +75 -0
- package/src/components/kind-documents.tsx +500 -0
- package/src/components/palette-action-publisher.tsx +140 -0
- package/src/components/primitives/DraftColorInput.tsx +74 -0
- package/src/components/product-shell.css +1829 -0
- package/src/components/project-tool-documents.tsx +410 -0
- package/src/components/scene-documents.tsx +223 -0
- package/src/components/stage-keyboard.tsx +37 -0
- package/src/components/stage-overlay-set.tsx +107 -0
- package/src/components/stage-presence-markers.ts +478 -0
- package/src/components/standard-viewport-dressing.ts +417 -0
- package/src/components/status-contributions.tsx +403 -0
- package/src/components/tool-documents.tsx +309 -0
- package/src/components/tool-schema-form.tsx +262 -0
- package/src/components/use-after-paint.ts +41 -0
- package/src/components/use-project-image-assets.ts +86 -0
- package/src/components/viewport-header-controls.css +90 -0
- package/src/components/viewport-surface-status.tsx +55 -0
- package/src/components/workspace-history.ts +32 -0
- package/src/components/workspace-static-panel-registry.tsx +152 -0
- package/src/components/workspace-surfaces.css +621 -0
- package/src/components/world-documents.tsx +586 -0
- package/src/components/world-overlay-gestures.ts +1694 -0
- package/src/composite-screenshot.ts +1641 -0
- package/src/console-sync.ts +131 -0
- package/src/constraint-helper.ts +338 -0
- package/src/content-entry-source-registry.ts +184 -0
- package/src/coverage/authoring-seam-evidence.ts +300 -0
- package/src/coverage/canvas-reveal.ts +192 -0
- package/src/coverage/design-time-surfaces.ts +101 -0
- package/src/coverage/live-seam-evidence.ts +11 -0
- package/src/coverage/ontology-invariants.ts +466 -0
- package/src/coverage/session-vitals.ts +501 -0
- package/src/coverage/system-seam-evidence.ts +72 -0
- package/src/crash-null-boundary.ts +36 -0
- package/src/creation-site-edit.ts +1479 -0
- package/src/creation-site-registry.ts +160 -0
- package/src/delegate-harness-registry.ts +143 -0
- package/src/document-context-registry.ts +177 -0
- package/src/document-open-registry.ts +200 -0
- package/src/document-preview-source.ts +20 -0
- package/src/document-renderer-session.ts +138 -0
- package/src/editor-api.ts +46 -0
- package/src/editor-chrome-capture.ts +139 -0
- package/src/editor-commands.ts +164 -0
- package/src/editor-console.ts +563 -0
- package/src/editor-current-view.ts +82 -0
- package/src/editor-document-probe.ts +881 -0
- package/src/editor-git-client.ts +115 -0
- package/src/editor-host-door.ts +462 -0
- package/src/editor-hotkeys.ts +842 -0
- package/src/editor-lease-view.ts +39 -0
- package/src/editor-lease.ts +415 -0
- package/src/editor-mode.ts +19 -0
- package/src/editor-notifications.ts +140 -0
- package/src/editor-presence.ts +563 -0
- package/src/editor-presentation-activity.ts +50 -0
- package/src/editor-presentation-notice.ts +42 -0
- package/src/editor-runtime.tsx +145 -0
- package/src/editor-server-response.ts +86 -0
- package/src/editor-session-attribution.ts +75 -0
- package/src/editor-session-mode.ts +54 -0
- package/src/editor-shell-store.ts +1304 -0
- package/src/editor-state-facets.ts +74 -0
- package/src/editor-styles.css +33 -0
- package/src/editor-view-presentation.ts +810 -0
- package/src/editor-viewport.ts +5302 -0
- package/src/entity-lod.ts +31 -0
- package/src/entity-object.ts +91 -0
- package/src/files/file-provider.ts +62 -0
- package/src/files/project-files.ts +264 -0
- package/src/finders/index.ts +136 -0
- package/src/finders/scenes-from-entrypoint-selection.ts +387 -0
- package/src/frame/bridge.tsx +1408 -0
- package/src/frame/product.ts +77 -0
- package/src/gameplay-dom-recording.ts +319 -0
- package/src/gameplay-export-state.ts +14 -0
- package/src/gameplay-replay.ts +417 -0
- package/src/gameplay-session-time.ts +9 -0
- package/src/gameplay-sessions.ts +204 -0
- package/src/harness-chat-types.ts +253 -0
- package/src/hierarchy-component-marks.ts +298 -0
- package/src/hierarchy-drop.ts +91 -0
- package/src/hierarchy-expansion-state.ts +80 -0
- package/src/hierarchy-header-slot.ts +52 -0
- package/src/hierarchy-internals.ts +197 -0
- package/src/hierarchy-kind-icon.ts +217 -0
- package/src/hierarchy-mark-reader.ts +73 -0
- package/src/hierarchy-menu-registry.ts +67 -0
- package/src/hierarchy-node-rows.ts +307 -0
- package/src/hierarchy-panel-view.ts +280 -0
- package/src/hierarchy-projection.ts +76 -0
- package/src/hierarchy-row-cache.ts +243 -0
- package/src/hierarchy-row-model.ts +308 -0
- package/src/hierarchy-rows.ts +11 -0
- package/src/hierarchy-walk.ts +86 -0
- package/src/history/editor-session.ts +25 -0
- package/src/history/history-commands.ts +147 -0
- package/src/history/history-delegate.ts +163 -0
- package/src/history/history-limit-notices.ts +43 -0
- package/src/history/history-service.ts +1173 -0
- package/src/history/persistence-coordinator.ts +35 -0
- package/src/history/project-file-history.ts +386 -0
- package/src/history/project-root-history-backends.ts +139 -0
- package/src/history/resource-registry.ts +209 -0
- package/src/history/snapshot-store.ts +103 -0
- package/src/history/source-history-backend.ts +546 -0
- package/src/history/types.ts +124 -0
- package/src/hmr-registration-group.ts +67 -0
- package/src/hmr-stable-react-context.ts +23 -0
- package/src/hotkeys.ts +188 -0
- package/src/inference-diagnostics.ts +69 -0
- package/src/initial-project.ts +80 -0
- package/src/inspection/active-subject.ts +578 -0
- package/src/inspection/active-surface.ts +142 -0
- package/src/inspection/compose.ts +1064 -0
- package/src/inspection/display.ts +170 -0
- package/src/inspection/document-subject.ts +109 -0
- package/src/inspection/game-subject.ts +85 -0
- package/src/inspection/model.ts +542 -0
- package/src/inspection/null-subject.ts +115 -0
- package/src/inspection/serialize.ts +357 -0
- package/src/inspection/use-active-inspection.ts +180 -0
- package/src/inspector-presentation.ts +201 -0
- package/src/inspector-section-registry.ts +221 -0
- package/src/instance-source-actions.ts +163 -0
- package/src/instanced-presentation.ts +164 -0
- package/src/js-heap.ts +71 -0
- package/src/key-actions.ts +91 -0
- package/src/keymap-presets.ts +401 -0
- package/src/layout-policy.ts +31 -0
- package/src/learn-links.ts +73 -0
- package/src/light-explorer-model.ts +134 -0
- package/src/live-canvas-frame.ts +55 -0
- package/src/live-document.ts +279 -0
- package/src/live-module-source.ts +230 -0
- package/src/live-session-registry.ts +220 -0
- package/src/live-transition.ts +633 -0
- package/src/manifest-project.ts +107 -0
- package/src/model-thumbnail.ts +523 -0
- package/src/native-selection-style.ts +202 -0
- package/src/object3d-document-write-policy.ts +137 -0
- package/src/packaged-runtime.ts +108 -0
- package/src/palettes/maya.palette.json +57 -0
- package/src/palettes/substance.palette.json +57 -0
- package/src/pasteboard-module.ts +81 -0
- package/src/performance-profiler.ts +367 -0
- package/src/performance-sources.ts +69 -0
- package/src/play-boot-phase.ts +145 -0
- package/src/presentation-surface.ts +248 -0
- package/src/project-adapter.ts +1143 -0
- package/src/project-asset-refresh.ts +26 -0
- package/src/project-declaration-refresh.ts +31 -0
- package/src/project-local-state.ts +118 -0
- package/src/project-manager.ts +243 -0
- package/src/project-module-changes.ts +163 -0
- package/src/project-module-split.ts +266 -0
- package/src/project-provenance.ts +115 -0
- package/src/project-ready.ts +42 -0
- package/src/project-session-reset.ts +67 -0
- package/src/project-shape.ts +68 -0
- package/src/project-tool-discovery.ts +66 -0
- package/src/project-tools.ts +107 -0
- package/src/project-work-types.ts +149 -0
- package/src/projection/three.ts +898 -0
- package/src/projection/types.ts +44 -0
- package/src/readiness.ts +113 -0
- package/src/reflection-probe-helper.ts +142 -0
- package/src/reported-play-state.ts +90 -0
- package/src/resolve-relative-specifier.ts +33 -0
- package/src/scene-document-plan.ts +315 -0
- package/src/scene-framing.ts +315 -0
- package/src/scene-live-open.ts +210 -0
- package/src/scene-view-fog.ts +89 -0
- package/src/scoped-game-css.ts +152 -0
- package/src/session-orphan-record.ts +193 -0
- package/src/session-tombstone.ts +126 -0
- package/src/settings/settings-provider.ts +82 -0
- package/src/settings-store.ts +345 -0
- package/src/shared-view-restore.ts +42 -0
- package/src/shell-store-door.ts +45 -0
- package/src/source-conflict.ts +122 -0
- package/src/spatial-handle-visuals.ts +332 -0
- package/src/stage-context.ts +341 -0
- package/src/stage-store-registry.ts +63 -0
- package/src/stale-chunk-recovery.ts +34 -0
- package/src/state-report-deferral.ts +73 -0
- package/src/storage/host-files-storage.ts +97 -0
- package/src/storage/http-storage.ts +174 -0
- package/src/storage/index.ts +75 -0
- package/src/storage/mem-storage.ts +149 -0
- package/src/storage/path-lock.ts +44 -0
- package/src/storage/paths.ts +26 -0
- package/src/storage/types.ts +116 -0
- package/src/stories/StoryComponentThumbnail.tsx +184 -0
- package/src/stories/StoryPreviewMount.tsx +306 -0
- package/src/stories/component-content-source.tsx +106 -0
- package/src/stories/component-states-source.ts +78 -0
- package/src/stories/compose-project-stories.ts +255 -0
- package/src/stories/pixi-story-model.ts +30 -0
- package/src/stories/prefabs-finder.ts +54 -0
- package/src/stories/prefabs-from-stories.ts +182 -0
- package/src/stories/project-story-discovery.ts +78 -0
- package/src/stories/project-story-regions.ts +24 -0
- package/src/stories/story-actions.ts +54 -0
- package/src/stories/story-capture-command.ts +124 -0
- package/src/stories/story-capture.ts +584 -0
- package/src/stories/story-declared-medium.ts +126 -0
- package/src/stories/story-discovery.ts +176 -0
- package/src/stories/story-document-openers.ts +36 -0
- package/src/stories/story-dom-runtime.ts +78 -0
- package/src/stories/story-grouping.ts +111 -0
- package/src/stories/story-lane.ts +88 -0
- package/src/stories/story-mount-turn.ts +27 -0
- package/src/stories/story-opener.ts +260 -0
- package/src/stories/story-pixi-preview.ts +408 -0
- package/src/stories/story-presentation.ts +215 -0
- package/src/stories/story-registry.ts +517 -0
- package/src/stories/story-three-preview.ts +807 -0
- package/src/stories/three-story-model.ts +96 -0
- package/src/story-three-preview-runtime.ts +56 -0
- package/src/surface-keyboard.ts +112 -0
- package/src/surface-state.ts +124 -0
- package/src/tab-bootstrap.js +628 -0
- package/src/tab-census.ts +198 -0
- package/src/tab-lifecycle-client.ts +196 -0
- package/src/theme-library.ts +844 -0
- package/src/theme-preference.ts +378 -0
- package/src/theme.css +3437 -0
- package/src/three-viewport/bone-selection-highlight.ts +119 -0
- package/src/three-viewport/camera-fit.ts +41 -0
- package/src/three-viewport/interactive-renderer.ts +132 -0
- package/src/three-viewport/selection-brackets.ts +310 -0
- package/src/three-viewport/selection-outline.ts +183 -0
- package/src/three-viewport/skeleton-helper.ts +61 -0
- package/src/three-viewport/source-color.ts +167 -0
- package/src/three-viewport/studio-environment.ts +96 -0
- package/src/three-viewport-presentation.ts +23 -0
- package/src/tool-contribution-play.ts +74 -0
- package/src/tool-loader.ts +1798 -0
- package/src/transform-mode-request.ts +57 -0
- package/src/transient-hint.ts +78 -0
- package/src/trigger-volume-helper.ts +116 -0
- package/src/ui-source/adapter-region-includes.ts +241 -0
- package/src/ui-source/ensure-import.ts +132 -0
- package/src/ui-source/file-region-resolver.ts +302 -0
- package/src/ui-source/inspect.ts +775 -0
- package/src/ui-source/oid-transform.ts +1845 -0
- package/src/ui-source/plan-csf-story.ts +196 -0
- package/src/ui-source/plan-extract-component.ts +421 -0
- package/src/ui-source/plan-fork-component.ts +621 -0
- package/src/ui-source/plan-named-style.ts +95 -0
- package/src/ui-source/plan-source-edit.ts +353 -0
- package/src/ui-source/r3f-contract-resolver.ts +327 -0
- package/src/ui-source/r3f-diagnostic-index.ts +165 -0
- package/src/ui-source/r3f-environment-binding.ts +133 -0
- package/src/ui-source/r3f-joint-binding.ts +251 -0
- package/src/ui-source/r3f-lod-binding.ts +91 -0
- package/src/ui-source/r3f-particle-binding.ts +235 -0
- package/src/ui-source/r3f-physics-binding.ts +483 -0
- package/src/ui-source/r3f-project-contracts.ts +444 -0
- package/src/ui-source/relative-import-specifier.ts +34 -0
- package/src/ui-source/reparent-guard.ts +544 -0
- package/src/ui-source/source-edit-request.ts +97 -0
- package/src/ui-source/source-write-backend.ts +618 -0
- package/src/ui-source/syntactic-prop-specs.ts +296 -0
- package/src/ui-source/tier-source-write-backend.ts +279 -0
- package/src/ui-source/ts-ast.ts +171 -0
- package/src/ui-source/utility-class-support.ts +169 -0
- package/src/ui-source/write-component-default.ts +133 -0
- package/src/ui-source/writer.ts +1989 -0
- package/src/viewport-activation-timings.ts +840 -0
- package/src/viewport-authoring-policy.ts +186 -0
- package/src/viewport-controls-hint.ts +57 -0
- package/src/viewport-door.ts +237 -0
- package/src/viewport-shading-boundary.ts +12 -0
- package/src/vite-error-surface.ts +141 -0
- package/src/wait-until.ts +37 -0
- package/src/workspace-areas.ts +156 -0
- package/src/workspace-aux-commands.ts +11 -0
- package/src/workspace-available-documents.ts +127 -0
- package/src/workspace-core-utilities.ts +31 -0
- package/src/workspace-document-ids.ts +59 -0
- package/src/workspace-document-registry.ts +612 -0
- package/src/workspace-document-restore.ts +145 -0
- package/src/workspace-host-commands.ts +141 -0
- package/src/workspace-persistence-gate.ts +40 -0
- package/src/workspace-play-utilities.ts +44 -0
- package/src/workspace-presets.ts +425 -0
- package/src/workspace-regions.ts +294 -0
- package/src/workspace-state-persistence.ts +536 -0
- package/src/workspace-static-panels.ts +73 -0
- package/src/workspace-status-registry.ts +121 -0
- package/src/workspace-style.ts +239 -0
- package/src/workspace-utility-commands.ts +74 -0
- package/src/workspace-utility-registry.ts +263 -0
- package/src/workspace-viewport-rect.ts +97 -0
- package/src/world-adoption.ts +115 -0
- package/src/world-document-routing.ts +104 -0
- package/vite-plugin-creation-site-write.ts +184 -0
- package/vite-plugin-creation-site.ts +80 -0
- package/vite-plugin-game-static.ts +303 -0
- package/vite-plugin-module-doorways.ts +336 -0
- package/vite-plugin-product-contributions.ts +197 -0
- package/vite-plugin-project-game-static.ts +125 -0
- package/vite-plugin-project-jsx-js.ts +65 -0
- package/vite-plugin-project-root-absolute-assets.ts +359 -0
- package/vite-plugin-shared-react.ts +420 -0
- package/vite-plugin-shared-three.ts +203 -0
- package/vite-plugin-ui-oid.ts +2106 -0
|
@@ -0,0 +1,968 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The RECORDER for a repo-vendored game's own source.
|
|
3
|
+
*
|
|
4
|
+
* A vendored game is somebody else's code we ship at the ZERO UNRECORDED DIFF
|
|
5
|
+
* bar (`vendor/games/verify-unaltered.mjs`): every byte in the folder is in
|
|
6
|
+
* exactly one declared bucket of `vendor/games/<id>.UPSTREAM.lock`, and each
|
|
7
|
+
* bucket is hashed. That bar has never forbidden divergence — it forbids
|
|
8
|
+
* SILENT divergence. So an authored edit to a vendored game is not a special
|
|
9
|
+
* case to be refused; it is an edit whose RECORDER is the lock instead of the
|
|
10
|
+
* user's own git.
|
|
11
|
+
*
|
|
12
|
+
* That is the whole content of this module. `server/creation-site-write.ts`
|
|
13
|
+
* decides whether an edit may be written at all (ownership, containment, the
|
|
14
|
+
* checksum guard) and `src/creation-site-edit.ts` decides what the new bytes
|
|
15
|
+
* are; when those two say yes and the target happens to live inside a vendored
|
|
16
|
+
* game, this module performs the write AND the lock update as one operation, so
|
|
17
|
+
* the folder is never in a state the verifier would call an unrecorded diff.
|
|
18
|
+
*
|
|
19
|
+
* ## What it writes into the lock
|
|
20
|
+
*
|
|
21
|
+
* Nothing new: the existing `patches` bucket, whose contract is exactly what an
|
|
22
|
+
* authored edit needs — a unified diff plus the two shas, with the verifier
|
|
23
|
+
* REVERSE-APPLYING the diff and checking the result re-derives upstream. The
|
|
24
|
+
* recorded diff IS the authoring edit, so that leg keeps working by
|
|
25
|
+
* construction. The reconciliation is a pure function of the file's bytes:
|
|
26
|
+
*
|
|
27
|
+
* bytes === upstream → the path belongs in `sha256_manifest`, patch deleted
|
|
28
|
+
* bytes !== upstream → the path belongs in `patches`, with a fresh diff
|
|
29
|
+
*
|
|
30
|
+
* which is what makes UNDO symmetric for free. Undo hands the previous bytes
|
|
31
|
+
* back through the same `/__ingest-source/apply` route, the reconciliation runs
|
|
32
|
+
* again, and the last undo — the one that restores upstream's own bytes —
|
|
33
|
+
* removes the patch entry and puts the path back in the manifest. No direction
|
|
34
|
+
* flag, no separate "unrecord" path that could disagree with the recording one.
|
|
35
|
+
*
|
|
36
|
+
* A `host_added` file (the contract shim) has no upstream at all, so its record
|
|
37
|
+
* is a single sha and that is what gets refreshed.
|
|
38
|
+
*
|
|
39
|
+
* ## Why upstream's bytes are never fetched
|
|
40
|
+
*
|
|
41
|
+
* They are already in the folder, twice over: for a pristine file the on-disk
|
|
42
|
+
* bytes ARE upstream (the manifest's sha proves it), and for an already-patched
|
|
43
|
+
* file reverse-applying the recorded diff reproduces them (the verifier's own
|
|
44
|
+
* leg 2, run here for the same reason). So the recorder is hermetic — no
|
|
45
|
+
* network, no clone — and it REFUSES rather than guesses when either derivation
|
|
46
|
+
* disagrees with the lock, because a lock that already disagrees with its
|
|
47
|
+
* folder is not a base anything may record onto.
|
|
48
|
+
*
|
|
49
|
+
* ## The crash window, and the journal that closes it
|
|
50
|
+
*
|
|
51
|
+
* Two files must move together and no filesystem moves two files atomically.
|
|
52
|
+
* The order used here is: plan everything (any refusal happens before a byte
|
|
53
|
+
* moves) → write a ROLL-FORWARD JOURNAL naming the complete end state → write
|
|
54
|
+
* the artifacts → delete the journal. A process that dies mid-write leaves the
|
|
55
|
+
* journal, and {@link settleVendoredWrite} — called by every `/__ingest-source/*`
|
|
56
|
+
* handler before it reads or writes the game — completes it. The journal lives
|
|
57
|
+
* in `<game>/.vgai/`, the host-state directory the verifier already excludes and
|
|
58
|
+
* git already ignores, so it is never itself a diff.
|
|
59
|
+
*
|
|
60
|
+
* ## Why every path here is bytes, not text
|
|
61
|
+
*
|
|
62
|
+
* A vendored game's files are not all source. A level-based game's placed
|
|
63
|
+
* objects live in a binary level file — megabytes of it — so authoring one is a
|
|
64
|
+
* write to a BINARY file and this module has to
|
|
65
|
+
* record it like any other. Two measured facts shape the code below:
|
|
66
|
+
*
|
|
67
|
+
* - **UTF-8 is lossy and silent.** Round-tripping a 2.3 MB binary level file
|
|
68
|
+
* through a string turned 2,339,773 bytes into 3,169,557. Nothing throws;
|
|
69
|
+
* the file is simply a
|
|
70
|
+
* different file. So every read, write, hash and journal field here carries
|
|
71
|
+
* `Buffer`, and the only `utf8` left is on the lock and the patch text, which
|
|
72
|
+
* are genuinely text.
|
|
73
|
+
* - **`git diff` says nothing about binary unless asked.** Without `--binary`
|
|
74
|
+
* it emits `Binary files a/x and b/x differ` — 233 bytes of prose that
|
|
75
|
+
* `git apply -R` cannot reverse, which would record a patch the verifier
|
|
76
|
+
* could never check. With `--binary` it emits a GIT binary patch carrying
|
|
77
|
+
* BOTH directions (a forward and a reverse delta, ~4.8 KB for a 12-byte
|
|
78
|
+
* change in that 2.3 MB file) and `git apply -R` reverses it exactly.
|
|
79
|
+
*
|
|
80
|
+
* `--binary` is therefore passed unconditionally rather than behind a
|
|
81
|
+
* content-type test: measured on a text file it produces byte-identical output
|
|
82
|
+
* to leaving it off, so one code path serves both and there is no sniffing rule
|
|
83
|
+
* to get wrong. The verifier needs no counterpart change — it already copies
|
|
84
|
+
* bytes (`copyFileSync`), already hashes a `Buffer`, and `git apply -R` is the
|
|
85
|
+
* same tool on either kind of patch.
|
|
86
|
+
*/
|
|
87
|
+
|
|
88
|
+
import { execFileSync } from 'node:child_process';
|
|
89
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
90
|
+
import {
|
|
91
|
+
existsSync,
|
|
92
|
+
mkdirSync,
|
|
93
|
+
mkdtempSync,
|
|
94
|
+
readdirSync,
|
|
95
|
+
readFileSync,
|
|
96
|
+
realpathSync,
|
|
97
|
+
renameSync,
|
|
98
|
+
rmSync,
|
|
99
|
+
statSync,
|
|
100
|
+
writeFileSync,
|
|
101
|
+
} from 'node:fs';
|
|
102
|
+
import { tmpdir } from 'node:os';
|
|
103
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Where every lock lives, whatever the game is or how it is vendored — and,
|
|
107
|
+
* for a `source-tree` game, where its folder lives too. Exported because
|
|
108
|
+
* `creation-site-write.ts` resolves a vendored anchor against this same home
|
|
109
|
+
* (a repo-vendored module's creation site is recorded relative to it, by
|
|
110
|
+
* `vite-plugin-creation-site` through `ingestGameShadowRoots`), and two
|
|
111
|
+
* spellings of one path is how they drift apart.
|
|
112
|
+
*/
|
|
113
|
+
export const LOCKS_DIR = ['vendor', 'games'];
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Host-authored state inside a game folder — the same two names
|
|
117
|
+
* `verify-unaltered.mjs` excludes for every game. Writing here is never a diff,
|
|
118
|
+
* so it is never recorded.
|
|
119
|
+
*/
|
|
120
|
+
const HOST_STATE_DIRS = ['.vgai', 'logs'];
|
|
121
|
+
|
|
122
|
+
/** `verify-unaltered.mjs`'s `DEFAULT_EXCLUDED_DIRS`, applied when a lock
|
|
123
|
+
* declares no `excluded` block of its own. */
|
|
124
|
+
const DEFAULT_EXCLUDED_DIRS = ['node_modules', 'dist', 'dist-ssr'];
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* One DEVIATION in a file's recorded diff.
|
|
128
|
+
*
|
|
129
|
+
* A patched file can carry more than one, applied at different times by
|
|
130
|
+
* different hands — `src/gameseq.js` carries a hand-written accessor-export
|
|
131
|
+
* deviation and, on top of it, an editor-authored anchor. The patch reverse-
|
|
132
|
+
* applies to upstream as a UNIT, so the two share one diff and one pair of
|
|
133
|
+
* shas; only the rationale is per-layer.
|
|
134
|
+
*
|
|
135
|
+
* `onto_sha256` is the file's content BEFORE this layer was applied, and it is
|
|
136
|
+
* what makes the stack reversible: an editor undo arrives as ordinary bytes,
|
|
137
|
+
* and the only way to know it removes the TOP layer rather than adding a third
|
|
138
|
+
* is that the incoming bytes hash to the top layer's own base.
|
|
139
|
+
*/
|
|
140
|
+
interface WhyLayer {
|
|
141
|
+
why: string;
|
|
142
|
+
onto_sha256: string;
|
|
143
|
+
/** True when the recorder wrote this layer's prose, false/absent for a
|
|
144
|
+
* rationale a person wrote. The recorder may re-word its own and must never
|
|
145
|
+
* re-word a person's. */
|
|
146
|
+
generated?: boolean;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
interface PatchSpec {
|
|
150
|
+
why: string;
|
|
151
|
+
/** Present only once a file carries MORE THAN ONE deviation, or once the
|
|
152
|
+
* recorder has added one of its own on top of an existing rationale — a
|
|
153
|
+
* single hand-written patch keeps the shape it has always had. */
|
|
154
|
+
why_layers?: WhyLayer[];
|
|
155
|
+
patch: string;
|
|
156
|
+
upstream_sha256: string;
|
|
157
|
+
patched_sha256: string;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
interface VendorLock {
|
|
161
|
+
id: string;
|
|
162
|
+
kind?: string;
|
|
163
|
+
bundle_dir?: string;
|
|
164
|
+
excluded?: { dirs?: string[]; files?: string[]; why?: string };
|
|
165
|
+
patches?: Record<string, PatchSpec>;
|
|
166
|
+
host_added?: { why?: string; files?: Record<string, string> };
|
|
167
|
+
tracked_file_count?: number;
|
|
168
|
+
sha256_manifest: Record<string, string>;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** A file that resolved inside a lock-recorded vendored game. */
|
|
172
|
+
export interface VendoredTarget {
|
|
173
|
+
readonly id: string;
|
|
174
|
+
/** Absolute path of `vendor/games/<id>.UPSTREAM.lock`. */
|
|
175
|
+
readonly lockPath: string;
|
|
176
|
+
/** Absolute path of the directory the lock's manifest is rooted at. */
|
|
177
|
+
readonly gameDir: string;
|
|
178
|
+
/** The file's POSIX path relative to `gameDir` — the key every bucket uses. */
|
|
179
|
+
readonly rel: string;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export type VendoredWriteResult =
|
|
183
|
+
| { readonly ok: true; readonly recorded: string }
|
|
184
|
+
| { readonly ok: false; readonly error: string };
|
|
185
|
+
|
|
186
|
+
/** Hashes BYTES, so it agrees with the verifier's `sha256(readFileSync(path))`
|
|
187
|
+
* for every file — text and binary alike. */
|
|
188
|
+
function sha256(bytes: Buffer): string {
|
|
189
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Git's own heuristic: a NUL byte near the front means "not text". Used only to
|
|
194
|
+
* describe an edit in the lock, never to decide how it is recorded — that path
|
|
195
|
+
* is one code path for both kinds on purpose.
|
|
196
|
+
*/
|
|
197
|
+
function looksBinary(bytes: Buffer): boolean {
|
|
198
|
+
return bytes.subarray(0, 8000).includes(0);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function posix(path: string): string {
|
|
202
|
+
return path.split(sep).join('/');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function isAtOrInside(parent: string, child: string): boolean {
|
|
206
|
+
const rel = relative(parent, child);
|
|
207
|
+
return rel === '' || (!rel.startsWith(`..${sep}`) && rel !== '..' && !isAbsolute(rel));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function readLock(lockPath: string): VendorLock {
|
|
211
|
+
return JSON.parse(readFileSync(lockPath, 'utf8')) as VendorLock;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** The directory a lock's manifest is rooted at — `gameDirFor` in the verifier,
|
|
215
|
+
* which is the one place this correspondence has to stay true. */
|
|
216
|
+
function gameDirFor(lock: VendorLock, engineRoot: string): string | null {
|
|
217
|
+
if ((lock.kind ?? 'source-tree') === 'bundle') {
|
|
218
|
+
return lock.bundle_dir ? resolve(engineRoot, lock.bundle_dir) : null;
|
|
219
|
+
}
|
|
220
|
+
return resolve(engineRoot, ...LOCKS_DIR, lock.id);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** One lock's IDENTITY — everything {@link findVendoredTarget} answers with,
|
|
224
|
+
* and nothing that a recorded write changes. */
|
|
225
|
+
interface VendoredRoot {
|
|
226
|
+
readonly id: string;
|
|
227
|
+
readonly lockPath: string;
|
|
228
|
+
/** Both spellings of the game's folder — see the symlink note below. */
|
|
229
|
+
readonly bases: readonly string[];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* The root table, memoized per locks directory — because resolving it costs
|
|
234
|
+
* 1.1 MB of JSON and ~28 `realpathSync` calls, and the answer for the
|
|
235
|
+
* overwhelmingly common input is `null`.
|
|
236
|
+
*
|
|
237
|
+
* MEASURED on this repo's 28-lock estate: an uncached lookup of a path in a
|
|
238
|
+
* user's own project — every read and every write the editor makes, since a
|
|
239
|
+
* vendored game is the rare case — cost **3.83 ms**, all of it spent proving a
|
|
240
|
+
* negative. Cached, the same lookup is a string-prefix walk. The write point
|
|
241
|
+
* paid this per write; once the READ points settle too (which is why this
|
|
242
|
+
* memo exists) it would have been paid per edit gesture as well.
|
|
243
|
+
*
|
|
244
|
+
* WHAT IS CACHED IS ONLY THE IDENTITY — `id`, `lockPath`, and the folder the
|
|
245
|
+
* manifest is rooted at. Those come from `id`/`kind`/`bundle_dir`, which a
|
|
246
|
+
* recorded write never touches; the mutable half (`patches`, `sha256_manifest`)
|
|
247
|
+
* is re-read from disk by every caller that needs it, so a recorded write can
|
|
248
|
+
* never be served a stale lock through here.
|
|
249
|
+
*
|
|
250
|
+
* REVALIDATION is the locks directory's own `mtimeMs` plus its entry list —
|
|
251
|
+
* two syscalls. Adding, removing or renaming a lock changes both, and the
|
|
252
|
+
* recorder writes locks through `renameSync` INTO this directory, so an
|
|
253
|
+
* ordinary recorded write bumps the mtime and refreshes the table on its own.
|
|
254
|
+
* The one edit this cannot see is a lock's `kind`/`bundle_dir` being rewritten
|
|
255
|
+
* IN PLACE by hand, which relocates an existing game's folder — a repo-level
|
|
256
|
+
* change, not a runtime one, and it takes a dev-server restart like every
|
|
257
|
+
* other.
|
|
258
|
+
*/
|
|
259
|
+
const rootTableByLocksDir = new Map<
|
|
260
|
+
string,
|
|
261
|
+
{ readonly mtimeMs: number; readonly names: string; readonly roots: readonly VendoredRoot[] }
|
|
262
|
+
>();
|
|
263
|
+
|
|
264
|
+
function vendoredRoots(engineRoot: string): readonly VendoredRoot[] {
|
|
265
|
+
const locksDir = resolve(engineRoot, ...LOCKS_DIR);
|
|
266
|
+
let mtimeMs: number;
|
|
267
|
+
let names: string[];
|
|
268
|
+
try {
|
|
269
|
+
mtimeMs = statSync(locksDir).mtimeMs;
|
|
270
|
+
names = readdirSync(locksDir).filter((name) => name.endsWith('.UPSTREAM.lock'));
|
|
271
|
+
} catch {
|
|
272
|
+
rootTableByLocksDir.delete(locksDir);
|
|
273
|
+
return [];
|
|
274
|
+
}
|
|
275
|
+
const key = names.join('');
|
|
276
|
+
const cached = rootTableByLocksDir.get(locksDir);
|
|
277
|
+
if (cached && cached.mtimeMs === mtimeMs && cached.names === key) return cached.roots;
|
|
278
|
+
|
|
279
|
+
const roots: VendoredRoot[] = [];
|
|
280
|
+
for (const name of names) {
|
|
281
|
+
const lockPath = join(locksDir, name);
|
|
282
|
+
let lock: VendorLock;
|
|
283
|
+
try {
|
|
284
|
+
lock = readLock(lockPath);
|
|
285
|
+
} catch {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const gameDir = gameDirFor(lock, engineRoot);
|
|
289
|
+
if (!gameDir) continue;
|
|
290
|
+
// `absPath` arrives realpath-resolved (the containment check in
|
|
291
|
+
// `creation-site-write.ts` is realpath-based), while `engineRoot` — and so
|
|
292
|
+
// `gameDir` — is whatever spelling the server was constructed with. On a
|
|
293
|
+
// checkout reached through a symlink (macOS `/tmp` → `/private/tmp` is the
|
|
294
|
+
// everyday case) the two spellings of the SAME directory diverge, and a
|
|
295
|
+
// prefix miss does not refuse anything: the caller falls through to
|
|
296
|
+
// the plain unrecorded write, which is exactly the silent-drift failure
|
|
297
|
+
// this module exists to make impossible. So both spellings are kept.
|
|
298
|
+
roots.push({ id: lock.id, lockPath, bases: [...new Set([gameDir, realOrSelf(gameDir)])] });
|
|
299
|
+
}
|
|
300
|
+
rootTableByLocksDir.set(locksDir, { mtimeMs, names: key, roots });
|
|
301
|
+
return roots;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* The lock-recorded vendored game whose folder contains `absPath` (or IS
|
|
306
|
+
* `absPath`), or `null` when no lock covers it — a user's own project, where
|
|
307
|
+
* their version control is the recorder and nothing here has anything to do.
|
|
308
|
+
*
|
|
309
|
+
* Pure: it reads locks and answers. The repair pass is
|
|
310
|
+
* {@link settleVendoredWrite}, deliberately a separate call so a lookup never
|
|
311
|
+
* has a side effect.
|
|
312
|
+
*/
|
|
313
|
+
export function findVendoredTarget(absPath: string, engineRoot: string): VendoredTarget | null {
|
|
314
|
+
for (const root of vendoredRoots(engineRoot)) {
|
|
315
|
+
for (const base of root.bases) {
|
|
316
|
+
if (!isAtOrInside(base, absPath)) continue;
|
|
317
|
+
return {
|
|
318
|
+
id: root.id,
|
|
319
|
+
lockPath: root.lockPath,
|
|
320
|
+
gameDir: base,
|
|
321
|
+
rel: posix(relative(base, absPath)),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** The realpath when the directory exists, the spelling itself when it does
|
|
329
|
+
* not — a missing dir cannot contain `absPath` either way. */
|
|
330
|
+
function realOrSelf(path: string): string {
|
|
331
|
+
try {
|
|
332
|
+
return realpathSync(path);
|
|
333
|
+
} catch {
|
|
334
|
+
return path;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ────────────────────────────────────────────────────────────── the journal
|
|
339
|
+
|
|
340
|
+
interface PendingVendoredWrite {
|
|
341
|
+
readonly version: 2;
|
|
342
|
+
readonly gameFile: string;
|
|
343
|
+
/** Base64, because the journal is JSON and the game file may be binary — a
|
|
344
|
+
* string field here would silently re-encode 2.3 MB of level data. */
|
|
345
|
+
readonly gameFileBase64: string;
|
|
346
|
+
readonly lockPath: string;
|
|
347
|
+
readonly lockContent: string;
|
|
348
|
+
/** Absolute path of the patch file, when this write has one to write OR to
|
|
349
|
+
* delete; `patchContent === null` means delete. */
|
|
350
|
+
readonly patchPath: string | null;
|
|
351
|
+
readonly patchContent: string | null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function journalPathFor(target: VendoredTarget): string {
|
|
355
|
+
return join(target.gameDir, '.vgai', 'vendored-lock-write.json');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function hostStateDir(target: VendoredTarget): string {
|
|
359
|
+
return join(target.gameDir, '.vgai');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Write `content` to `path` through a rename, staging the temporary file in the
|
|
364
|
+
* game's own host-state directory so a torn write is never visible AS A FILE to
|
|
365
|
+
* the verifier's directory walk (which excludes `.vgai/`) or to git.
|
|
366
|
+
*/
|
|
367
|
+
function writeThroughRename(path: string, content: Buffer, stageDir: string): void {
|
|
368
|
+
mkdirSync(stageDir, { recursive: true });
|
|
369
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
370
|
+
const staged = join(stageDir, `stage-${process.pid}-${randomUUID()}.tmp`);
|
|
371
|
+
writeFileSync(staged, content);
|
|
372
|
+
renameSync(staged, path);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** Text artifacts — the lock and the patch — are genuinely text; only the game
|
|
376
|
+
* file itself may be binary. */
|
|
377
|
+
function writeTextThroughRename(path: string, content: string, stageDir: string): void {
|
|
378
|
+
writeThroughRename(path, Buffer.from(content, 'utf8'), stageDir);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function applyPending(pending: PendingVendoredWrite, stageDir: string): void {
|
|
382
|
+
if (pending.patchPath) {
|
|
383
|
+
if (pending.patchContent === null) rmSync(pending.patchPath, { force: true });
|
|
384
|
+
else writeTextThroughRename(pending.patchPath, pending.patchContent, stageDir);
|
|
385
|
+
}
|
|
386
|
+
writeTextThroughRename(pending.lockPath, pending.lockContent, stageDir);
|
|
387
|
+
writeThroughRename(pending.gameFile, Buffer.from(pending.gameFileBase64, 'base64'), stageDir);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Finish any write that died between its artifacts, so no caller ever reads or
|
|
392
|
+
* writes a vendored game whose lock and bytes disagree because of us.
|
|
393
|
+
*
|
|
394
|
+
* Roll-FORWARD only, and therefore idempotent: the journal names the complete
|
|
395
|
+
* end state, so re-writing all of it is always correct no matter how far the
|
|
396
|
+
* interrupted attempt got.
|
|
397
|
+
*
|
|
398
|
+
* Returns whether it actually rolled anything forward. That answer is not
|
|
399
|
+
* bookkeeping: settling REPLACES THE FILE'S BYTES, so a caller that serves
|
|
400
|
+
* modules (the dev server) has to invalidate what it had cached for that file,
|
|
401
|
+
* exactly as it does after a write of its own. `false` — the ordinary case —
|
|
402
|
+
* means nothing moved and there is nothing to invalidate.
|
|
403
|
+
*/
|
|
404
|
+
export function settleVendoredWrite(target: VendoredTarget): boolean {
|
|
405
|
+
const journal = journalPathFor(target);
|
|
406
|
+
if (!existsSync(journal)) return false;
|
|
407
|
+
const pending = JSON.parse(readFileSync(journal, 'utf8')) as PendingVendoredWrite;
|
|
408
|
+
if (pending.version !== 2) {
|
|
409
|
+
// FAIL CLOSED, LOUDLY. A journal exists because a write may have died
|
|
410
|
+
// between the game's bytes and its lock; one this build cannot read means
|
|
411
|
+
// we do not know whether they agree, and every route calls this precisely so
|
|
412
|
+
// nothing plans, reads or writes against bytes in that state. Returning
|
|
413
|
+
// quietly — which is what this did — let the very next request act on a
|
|
414
|
+
// possibly half-written vendored game with no signal anywhere.
|
|
415
|
+
const message =
|
|
416
|
+
`${target.id}: ${journal} is a pending vendored write this build cannot settle ` +
|
|
417
|
+
`(journal version ${String(pending.version)}, expected 2). The game's bytes and its lock ` +
|
|
418
|
+
'may disagree. Check the game with `node vendor/games/verify-unaltered.mjs`, then delete ' +
|
|
419
|
+
'the journal once the tree is consistent.';
|
|
420
|
+
console.error(`[vendored-lock-recorder] ${message}`);
|
|
421
|
+
throw new Error(message);
|
|
422
|
+
}
|
|
423
|
+
applyPending(pending, hostStateDir(target));
|
|
424
|
+
rmSync(journal, { force: true });
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ─────────────────────────────────────────────────────────── diff plumbing
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* `git`, run in a throwaway directory OUTSIDE any working tree.
|
|
432
|
+
*
|
|
433
|
+
* The verifier reverse-applies recorded diffs with `git apply -R`, so the
|
|
434
|
+
* recorded diffs are produced by `git diff` — the same tool, at zero fuzz. A
|
|
435
|
+
* hand-rolled unified diff would be a second implementation of the format the
|
|
436
|
+
* gate reads, which is the one place a subtle disagreement would show up as a
|
|
437
|
+
* red gate nobody could explain.
|
|
438
|
+
*/
|
|
439
|
+
function inScratch<T>(body: (dir: string) => T): T {
|
|
440
|
+
const dir = mkdtempSync(join(tmpdir(), 'vgai-vendored-record-'));
|
|
441
|
+
try {
|
|
442
|
+
return body(dir);
|
|
443
|
+
} finally {
|
|
444
|
+
rmSync(dir, { recursive: true, force: true });
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* `git`, forced to treat the scratch directory as OUTSIDE every repository —
|
|
450
|
+
* enforced, not assumed.
|
|
451
|
+
*
|
|
452
|
+
* `tmpdir()` is `TMPDIR`, and `TMPDIR` can point INSIDE a working tree (an
|
|
453
|
+
* agent worktree sets a short one because a checkout path can overrun the
|
|
454
|
+
* ~104-byte unix-socket limit). When it does, `git apply -R` resolves the
|
|
455
|
+
* patch's paths against the enclosing repo's TOPLEVEL instead of `cwd`, finds
|
|
456
|
+
* nothing to change, and EXITS 0 — measured: the reverse-apply returned the
|
|
457
|
+
* patched bytes unchanged, so `upstreamBytesOf` computed a sha that was not
|
|
458
|
+
* upstream's and every write to that game was refused with "the lock is not a
|
|
459
|
+
* base anything may record onto", which names the wrong thing entirely.
|
|
460
|
+
*
|
|
461
|
+
* `GIT_CEILING_DIRECTORIES` at the scratch dir's PARENT stops repository
|
|
462
|
+
* discovery before it can leave the scratch dir, so the same command behaves
|
|
463
|
+
* identically wherever `TMPDIR` points. (`--unsafe-paths` was measured NOT to
|
|
464
|
+
* fix it; a `git init` in the scratch dir does, at the cost of a second
|
|
465
|
+
* process.)
|
|
466
|
+
*/
|
|
467
|
+
function gitInScratch(dir: string, args: readonly string[]): string {
|
|
468
|
+
return execFileSync('git', args, {
|
|
469
|
+
cwd: dir,
|
|
470
|
+
env: { ...process.env, GIT_CEILING_DIRECTORIES: dirname(dir) },
|
|
471
|
+
encoding: 'utf8',
|
|
472
|
+
maxBuffer: 1 << 28,
|
|
473
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** A unified diff taking `before` to `after` for `rel`, in the `a/`+`b/` shape
|
|
478
|
+
* the recorded patches already use, or `null` when the two are identical. */
|
|
479
|
+
function unifiedDiff(rel: string, before: Buffer, after: Buffer): string | null {
|
|
480
|
+
if (before.equals(after)) return null;
|
|
481
|
+
return inScratch((dir) => {
|
|
482
|
+
const a = join(dir, 'a', rel);
|
|
483
|
+
const b = join(dir, 'b', rel);
|
|
484
|
+
mkdirSync(dirname(a), { recursive: true });
|
|
485
|
+
mkdirSync(dirname(b), { recursive: true });
|
|
486
|
+
writeFileSync(a, before);
|
|
487
|
+
writeFileSync(b, after);
|
|
488
|
+
let out = '';
|
|
489
|
+
try {
|
|
490
|
+
// `--binary` is what makes this work for a binary level file: without
|
|
491
|
+
// it git emits only `Binary files … differ`, which reverse-applies to
|
|
492
|
+
// nothing. On text it changes the output not at all.
|
|
493
|
+
out = gitInScratch(dir, [
|
|
494
|
+
'diff',
|
|
495
|
+
'--binary',
|
|
496
|
+
'--no-index',
|
|
497
|
+
'--no-prefix',
|
|
498
|
+
'--no-color',
|
|
499
|
+
'--',
|
|
500
|
+
`a/${rel}`,
|
|
501
|
+
`b/${rel}`,
|
|
502
|
+
]);
|
|
503
|
+
} catch (error) {
|
|
504
|
+
// `git diff --no-index` exits 1 WHEN THERE IS A DIFFERENCE — the normal
|
|
505
|
+
// case here. The diff text is still on stdout; only an empty stdout is a
|
|
506
|
+
// real failure.
|
|
507
|
+
out = String((error as { stdout?: string }).stdout ?? '');
|
|
508
|
+
}
|
|
509
|
+
if (!out.trim()) throw new Error(`git produced no diff for ${rel}`);
|
|
510
|
+
if (/^Binary files .* differ$/m.test(out)) {
|
|
511
|
+
// Belt and braces: if this ever reappears, the recorded patch would be
|
|
512
|
+
// unreversible and the verifier would fail later, on somebody else's
|
|
513
|
+
// commit. Refuse here instead, where the cause is still visible.
|
|
514
|
+
throw new Error(
|
|
515
|
+
`git described the change to ${rel} only as "Binary files differ" — that cannot be ` +
|
|
516
|
+
'reverse-applied, so it is not a record and will not be written',
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
return out;
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** The content reverse-applying `patchText` to `patched` yields — the verifier's
|
|
524
|
+
* own leg 2, returning the bytes instead of their digest. */
|
|
525
|
+
function reverseApply(rel: string, patched: Buffer, patchText: string): Buffer {
|
|
526
|
+
return inScratch((dir) => {
|
|
527
|
+
const target = join(dir, rel);
|
|
528
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
529
|
+
writeFileSync(target, patched);
|
|
530
|
+
writeFileSync(join(dir, '.recorded.patch'), patchText, 'utf8');
|
|
531
|
+
gitInScratch(dir, ['apply', '-R', '--whitespace=nowarn', '.recorded.patch']);
|
|
532
|
+
return readFileSync(target);
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** `src/render.js` → `src-render.js.patch`, the convention already on disk. */
|
|
537
|
+
function patchFileNameFor(rel: string): string {
|
|
538
|
+
return `${rel.split('/').join('-')}.patch`;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** Reuse a patch record's own path once it exists. For a new record, keep the
|
|
542
|
+
* readable convention unless another source path already owns that name; the
|
|
543
|
+
* digest suffix makes the collision resolution deterministic and injective. */
|
|
544
|
+
function patchPathFor(
|
|
545
|
+
lock: VendorLock,
|
|
546
|
+
target: VendoredTarget,
|
|
547
|
+
bucket: 'files' | 'patches',
|
|
548
|
+
): string {
|
|
549
|
+
const lockDir = dirname(target.lockPath);
|
|
550
|
+
if (bucket === 'patches') return join(lockDir, lock.patches![target.rel]!.patch);
|
|
551
|
+
const directory = join(lockDir, `${target.id}.patches`);
|
|
552
|
+
const readable = patchFileNameFor(target.rel);
|
|
553
|
+
const relativeCandidate = posix(relative(lockDir, join(directory, readable)));
|
|
554
|
+
const occupied = new Set(Object.values(lock.patches ?? {}).map((spec) => spec.patch));
|
|
555
|
+
if (!occupied.has(relativeCandidate)) return join(directory, readable);
|
|
556
|
+
const suffix = sha256(Buffer.from(target.rel, 'utf8')).slice(0, 10);
|
|
557
|
+
return join(directory, readable.replace(/\.patch$/, `-${suffix}.patch`));
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// ─────────────────────────────────────────────────────────── reconciliation
|
|
561
|
+
|
|
562
|
+
type Bucket = 'files' | 'patches' | 'host_added' | 'excluded' | 'undeclared';
|
|
563
|
+
|
|
564
|
+
function bucketOf(lock: VendorLock, rel: string): Bucket {
|
|
565
|
+
const top = rel.split('/')[0] ?? '';
|
|
566
|
+
const excludedDirs = new Set([
|
|
567
|
+
...(lock.excluded?.dirs ?? DEFAULT_EXCLUDED_DIRS),
|
|
568
|
+
...HOST_STATE_DIRS,
|
|
569
|
+
]);
|
|
570
|
+
if (excludedDirs.has(top)) return 'excluded';
|
|
571
|
+
if ((lock.excluded?.files ?? []).includes(rel)) return 'excluded';
|
|
572
|
+
if (lock.sha256_manifest[rel] !== undefined) return 'files';
|
|
573
|
+
if (lock.patches?.[rel] !== undefined) return 'patches';
|
|
574
|
+
if (lock.host_added?.files?.[rel] !== undefined) return 'host_added';
|
|
575
|
+
return 'undeclared';
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Put `key` back into a key-ordered map at the position it sorts to, leaving
|
|
580
|
+
* every other key's relative order alone.
|
|
581
|
+
*
|
|
582
|
+
* The manifests on disk are ASCII-sorted, and an undo that returns a path to
|
|
583
|
+
* the manifest should leave a one-line diff there rather than a re-ordered
|
|
584
|
+
* file. Appending would do that today and drift forever after.
|
|
585
|
+
*/
|
|
586
|
+
function insertOrdered<T>(map: Record<string, T>, key: string, value: T): Record<string, T> {
|
|
587
|
+
const out: Record<string, T> = {};
|
|
588
|
+
let placed = false;
|
|
589
|
+
for (const [existing, existingValue] of Object.entries(map)) {
|
|
590
|
+
if (!placed && existing > key) {
|
|
591
|
+
out[key] = value;
|
|
592
|
+
placed = true;
|
|
593
|
+
}
|
|
594
|
+
out[existing] = existingValue;
|
|
595
|
+
}
|
|
596
|
+
if (!placed) out[key] = value;
|
|
597
|
+
return out;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/** The generated `why` for an editor-authored deviation. Names the edit in the
|
|
601
|
+
* terms a reviewer reads the lock in: what moved, where, and by whom. */
|
|
602
|
+
function describeEdit(
|
|
603
|
+
rel: string,
|
|
604
|
+
note: string | undefined,
|
|
605
|
+
before: Buffer,
|
|
606
|
+
after: Buffer,
|
|
607
|
+
): string {
|
|
608
|
+
// Where the bytes moved, in the terms the file itself is read in: a line
|
|
609
|
+
// number for source, a byte offset for data. Naming a "line" in a 2.3 MB
|
|
610
|
+
// level archive would be a number nobody could look up.
|
|
611
|
+
const where = (() => {
|
|
612
|
+
if (looksBinary(before) || looksBinary(after)) {
|
|
613
|
+
let at = 0;
|
|
614
|
+
while (at < before.length && at < after.length && before[at] === after[at]) at++;
|
|
615
|
+
return `byte ${at} (binary; ${before.length} → ${after.length} bytes)`;
|
|
616
|
+
}
|
|
617
|
+
const a = before.toString('utf8').split('\n');
|
|
618
|
+
const b = after.toString('utf8').split('\n');
|
|
619
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
620
|
+
if (a[i] !== b[i]) return `${rel}:${i + 1}`;
|
|
621
|
+
}
|
|
622
|
+
return `${rel}:1`;
|
|
623
|
+
})();
|
|
624
|
+
// The note names the EDIT (which property, at which creation site); the
|
|
625
|
+
// derived location names where the bytes actually moved, which is not the same
|
|
626
|
+
// place — a property is often written a line or two below its `new`.
|
|
627
|
+
const what = note ? `${note}; ` : '';
|
|
628
|
+
return (
|
|
629
|
+
`EDITOR-AUTHORED (creation-site write-back): ${what}changed at ${where}. ` +
|
|
630
|
+
'Written into the game’s own source by the editor at the moment of the edit, and recorded ' +
|
|
631
|
+
'here in the same operation — the diff IS the authoring edit, so it reverse-applies to ' +
|
|
632
|
+
'upstream by construction.'
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* The promise a SINGLE recorder-authored deviation may make, spelled once
|
|
638
|
+
* because it is both appended (when composing one layer) and stripped (when an
|
|
639
|
+
* older single-layer record becomes layer one of a stack, where it stops being
|
|
640
|
+
* true).
|
|
641
|
+
*/
|
|
642
|
+
const UNDO_TO_UPSTREAM =
|
|
643
|
+
' Undoing the edit in the editor removes this entry and returns the path to `sha256_manifest`.';
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* The `why` a stack of layers reads as — and the ONE place the undo promise is
|
|
647
|
+
* made, because that promise depends on how many layers there are.
|
|
648
|
+
*
|
|
649
|
+
* A single layer keeps the shape a lock has always had. A later layer is
|
|
650
|
+
* APPENDED, and deliberately not renumbered into a list: the prose already
|
|
651
|
+
* there may itself describe several deviations (`src/gameseq.js`'s does), and
|
|
652
|
+
* re-labelling it "(1)" produced a record that opened "2 layered deviations …
|
|
653
|
+
* (1) TWO layered deviations …". Leaving what someone wrote exactly as they
|
|
654
|
+
* wrote it and adding after it reads correctly however many they described.
|
|
655
|
+
*
|
|
656
|
+
* The tail states the real undo condition, which is the defect this exists to
|
|
657
|
+
* prevent: a `why` that called an accessor-export patch editor-authored and
|
|
658
|
+
* promised an undo straight back to `sha256_manifest` that could not happen
|
|
659
|
+
* while the accessor hunks remained.
|
|
660
|
+
*/
|
|
661
|
+
function composeWhy(layers: readonly WhyLayer[]): string {
|
|
662
|
+
const [first, ...rest] = layers;
|
|
663
|
+
if (!first) {
|
|
664
|
+
throw new Error(
|
|
665
|
+
'a recorded patch always has at least one deviation to explain — a stack that emptied ' +
|
|
666
|
+
'means the file is back to upstream, which removes the record rather than re-writing it',
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
if (rest.length === 0) return first.generated ? `${first.why}${UNDO_TO_UPSTREAM}` : first.why;
|
|
670
|
+
const added = rest.map((layer) => `AND, ON TOP OF THAT: ${layer.why}`).join(' ');
|
|
671
|
+
return (
|
|
672
|
+
`${first.why} ${added} These deviations were recorded separately and share one diff (the ` +
|
|
673
|
+
'patch reverse-applies to upstream as a unit). Undoing the last edit in the editor removes ' +
|
|
674
|
+
'only its own layer; the path returns to `sha256_manifest` only when EVERY deviation above ' +
|
|
675
|
+
'is gone.'
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* The layer stack a patch entry already carries.
|
|
681
|
+
*
|
|
682
|
+
* A lock written before layering — or by hand — has a `why` and no `why_layers`,
|
|
683
|
+
* and that `why` is layer one: it describes the whole deviation as it stands,
|
|
684
|
+
* applied onto upstream. Seeding it that way is what lets a person's rationale
|
|
685
|
+
* survive the next editor edit intact, which is the whole point.
|
|
686
|
+
*/
|
|
687
|
+
function existingLayers(spec: PatchSpec | undefined, upstreamSha: string): WhyLayer[] {
|
|
688
|
+
if (!spec) return [];
|
|
689
|
+
if (spec.why_layers && spec.why_layers.length > 0) return spec.why_layers.map((l) => ({ ...l }));
|
|
690
|
+
// A single-layer record the recorder itself wrote ends with a promise that is
|
|
691
|
+
// only true while it IS the only layer. Strip it here — on an EXACT match with
|
|
692
|
+
// our own sentence, so a person's prose is never touched — and let
|
|
693
|
+
// `composeWhy` re-add or replace it from the stack's real shape.
|
|
694
|
+
return spec.why.endsWith(UNDO_TO_UPSTREAM.trimStart())
|
|
695
|
+
? [
|
|
696
|
+
{
|
|
697
|
+
why: spec.why.slice(0, -UNDO_TO_UPSTREAM.trimStart().length).trimEnd(),
|
|
698
|
+
onto_sha256: upstreamSha,
|
|
699
|
+
generated: true,
|
|
700
|
+
},
|
|
701
|
+
]
|
|
702
|
+
: [{ why: spec.why, onto_sha256: upstreamSha }];
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
interface Reconciled {
|
|
706
|
+
readonly lock: VendorLock;
|
|
707
|
+
readonly patchPath: string | null;
|
|
708
|
+
readonly patchContent: string | null;
|
|
709
|
+
readonly recorded: string;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* The buckets with no upstream counterpart to diff against.
|
|
714
|
+
*
|
|
715
|
+
* `host_added` is a file WE ship (the contract shim), so its record is one sha
|
|
716
|
+
* and re-hashing it is the whole update. The other two are not records at all:
|
|
717
|
+
* a path the lock has never heard of is ALREADY an unrecorded diff, and a path
|
|
718
|
+
* the lock excludes is outside the estate's accounting entirely — writing
|
|
719
|
+
* either through the recorder would be claiming an accounting that does not
|
|
720
|
+
* exist, so both refuse by name.
|
|
721
|
+
*/
|
|
722
|
+
function reconcileWithoutUpstream(
|
|
723
|
+
next: VendorLock,
|
|
724
|
+
target: VendoredTarget,
|
|
725
|
+
current: Buffer,
|
|
726
|
+
after: Buffer,
|
|
727
|
+
bucket: Exclude<Bucket, 'files' | 'patches'>,
|
|
728
|
+
): Reconciled {
|
|
729
|
+
const rel = target.rel;
|
|
730
|
+
if (bucket === 'undeclared') {
|
|
731
|
+
throw new Error(
|
|
732
|
+
`${rel} is not declared in ${target.id}'s lock — a file the lock does not know about ` +
|
|
733
|
+
'is already an unrecorded diff, so the editor will not add a second one on top of it',
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
if (bucket === 'excluded') {
|
|
737
|
+
throw new Error(`${rel} is excluded from ${target.id}'s lock and needs no record`);
|
|
738
|
+
}
|
|
739
|
+
const recorded = next.host_added?.files?.[rel];
|
|
740
|
+
if (!recorded || sha256(current) !== recorded) {
|
|
741
|
+
throw new Error(
|
|
742
|
+
`${rel} does not match the sha256 ${target.id}'s host_added record claims — the file ` +
|
|
743
|
+
'already changed outside the recorder, so this edit will not absorb that divergence',
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
const files = { ...(next.host_added?.files ?? {}) };
|
|
747
|
+
files[rel] = sha256(after);
|
|
748
|
+
next.host_added = { ...(next.host_added ?? {}), files };
|
|
749
|
+
return {
|
|
750
|
+
lock: next,
|
|
751
|
+
patchPath: null,
|
|
752
|
+
patchContent: null,
|
|
753
|
+
recorded: `${target.id}: host-added ${rel} re-hashed in the lock`,
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Upstream's own bytes for a file that is on disk right now, derived from the
|
|
759
|
+
* folder itself — no network, no clone.
|
|
760
|
+
*
|
|
761
|
+
* Both derivations are CHECKED against the lock, and a disagreement throws
|
|
762
|
+
* rather than proceeds: recording onto a base that already diverges from its
|
|
763
|
+
* lock would launder somebody else's unrecorded diff into a sanctioned one,
|
|
764
|
+
* which is the one failure mode that would make this whole lane worse than the
|
|
765
|
+
* refusal it replaces.
|
|
766
|
+
*/
|
|
767
|
+
function upstreamBytesOf(
|
|
768
|
+
lock: VendorLock,
|
|
769
|
+
target: VendoredTarget,
|
|
770
|
+
current: Buffer,
|
|
771
|
+
bucket: 'files' | 'patches',
|
|
772
|
+
): Buffer {
|
|
773
|
+
const rel = target.rel;
|
|
774
|
+
if (bucket === 'files') {
|
|
775
|
+
if (sha256(current) !== lock.sha256_manifest[rel]) {
|
|
776
|
+
throw new Error(
|
|
777
|
+
`${rel} does not match the sha256 ${target.id}'s lock records for it — the folder ` +
|
|
778
|
+
'already diverges from its own lock, so nothing may be recorded onto it',
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
return current;
|
|
782
|
+
}
|
|
783
|
+
const spec = lock.patches![rel]!;
|
|
784
|
+
const patchPath = join(dirname(target.lockPath), spec.patch);
|
|
785
|
+
if (!existsSync(patchPath)) {
|
|
786
|
+
throw new Error(`${target.id}'s lock points at a missing patch file for ${rel}`);
|
|
787
|
+
}
|
|
788
|
+
if (sha256(current) !== spec.patched_sha256) {
|
|
789
|
+
throw new Error(
|
|
790
|
+
`${rel} does not match the \`patched_sha256\` ${target.id}'s lock records for it — ` +
|
|
791
|
+
'the folder already diverges from its own lock',
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
const upstream = reverseApply(rel, current, readFileSync(patchPath, 'utf8'));
|
|
795
|
+
if (sha256(upstream) !== spec.upstream_sha256) {
|
|
796
|
+
throw new Error(
|
|
797
|
+
`${target.id}'s recorded patch for ${rel} does not reverse-apply to the upstream sha ` +
|
|
798
|
+
'it claims — the lock is not a base anything may record onto',
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
return upstream;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* The lock as it must read once `after` is the file's content — the pure half,
|
|
806
|
+
* so a test drives exactly what the server does.
|
|
807
|
+
*/
|
|
808
|
+
export function reconcileLock(
|
|
809
|
+
lock: VendorLock,
|
|
810
|
+
target: VendoredTarget,
|
|
811
|
+
current: Buffer,
|
|
812
|
+
after: Buffer,
|
|
813
|
+
note: string | undefined,
|
|
814
|
+
): Reconciled {
|
|
815
|
+
const rel = target.rel;
|
|
816
|
+
const next: VendorLock = JSON.parse(JSON.stringify(lock)) as VendorLock;
|
|
817
|
+
const bucket = bucketOf(lock, rel);
|
|
818
|
+
if (bucket !== 'files' && bucket !== 'patches') {
|
|
819
|
+
return reconcileWithoutUpstream(next, target, current, after, bucket);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const upstream = upstreamBytesOf(lock, target, current, bucket);
|
|
823
|
+
const patchFile = patchPathFor(lock, target, bucket);
|
|
824
|
+
|
|
825
|
+
if (sha256(after) === sha256(upstream)) {
|
|
826
|
+
// Back to upstream's own bytes: the deviation is gone, so its record goes
|
|
827
|
+
// with it and the path returns to the manifest.
|
|
828
|
+
if (next.patches) {
|
|
829
|
+
delete next.patches[rel];
|
|
830
|
+
if (Object.keys(next.patches).length === 0) delete next.patches;
|
|
831
|
+
}
|
|
832
|
+
next.sha256_manifest = insertOrdered(next.sha256_manifest, rel, sha256(upstream));
|
|
833
|
+
syncTrackedCount(lock, next);
|
|
834
|
+
return {
|
|
835
|
+
lock: next,
|
|
836
|
+
patchPath: bucket === 'patches' ? patchFile : null,
|
|
837
|
+
patchContent: null,
|
|
838
|
+
recorded: `${target.id}: ${rel} is upstream’s own bytes again — its patch record removed`,
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const patchText = unifiedDiff(rel, upstream, after);
|
|
843
|
+
if (patchText === null) throw new Error(`${rel} produced no diff against upstream`);
|
|
844
|
+
|
|
845
|
+
// LAYERING, under one rule: THE RECORDER OWNS AT MOST ONE LAYER — the top one
|
|
846
|
+
// — and never touches anything below it.
|
|
847
|
+
//
|
|
848
|
+
// - Over a rationale a person wrote, an editor edit ADDS a layer. Replacing
|
|
849
|
+
// it was the shipped defect: it called an accessor-export patch
|
|
850
|
+
// editor-authored and promised an undo straight back to `sha256_manifest`
|
|
851
|
+
// that could not happen while the accessor hunks remained.
|
|
852
|
+
// - Over the recorder's OWN layer, a further editor edit REPLACES it, keeping
|
|
853
|
+
// that layer's `onto_sha256`. A second drag of the same robot is not a
|
|
854
|
+
// second deviation, and a `why` that grew a paragraph per gesture would be
|
|
855
|
+
// unreadable within a session.
|
|
856
|
+
// - Bytes that hash to the top layer's own base are an UNDO of that layer:
|
|
857
|
+
// pop it, and the rationale underneath comes back exactly as it was.
|
|
858
|
+
const layers = existingLayers(lock.patches?.[rel], sha256(upstream));
|
|
859
|
+
const top = layers[layers.length - 1];
|
|
860
|
+
const undoingTop = top !== undefined && sha256(after) === top.onto_sha256;
|
|
861
|
+
const fresh: WhyLayer = {
|
|
862
|
+
why: describeEdit(rel, note, upstream, after),
|
|
863
|
+
// The base is the state before the recorder's layer BEGAN, not before this
|
|
864
|
+
// particular gesture — otherwise re-dragging an object would move the undo
|
|
865
|
+
// target forward and the first edit could never be popped.
|
|
866
|
+
onto_sha256: top?.generated ? top.onto_sha256 : sha256(current),
|
|
867
|
+
generated: true,
|
|
868
|
+
};
|
|
869
|
+
const nextLayers: WhyLayer[] = undoingTop
|
|
870
|
+
? layers.slice(0, -1)
|
|
871
|
+
: top?.generated
|
|
872
|
+
? [...layers.slice(0, -1), fresh]
|
|
873
|
+
: [...layers, fresh];
|
|
874
|
+
|
|
875
|
+
const spec: PatchSpec = {
|
|
876
|
+
why: composeWhy(nextLayers),
|
|
877
|
+
// The array is the machine-readable half and only earns its place once
|
|
878
|
+
// there is something a single `why` cannot express.
|
|
879
|
+
...(nextLayers.length > 1 ? { why_layers: nextLayers } : {}),
|
|
880
|
+
patch: posix(relative(dirname(target.lockPath), patchFile)),
|
|
881
|
+
upstream_sha256: sha256(upstream),
|
|
882
|
+
patched_sha256: sha256(after),
|
|
883
|
+
};
|
|
884
|
+
next.patches = { ...(next.patches ?? {}) };
|
|
885
|
+
next.patches[rel] = spec;
|
|
886
|
+
if (bucket === 'files') {
|
|
887
|
+
delete next.sha256_manifest[rel];
|
|
888
|
+
syncTrackedCount(lock, next);
|
|
889
|
+
}
|
|
890
|
+
return {
|
|
891
|
+
lock: next,
|
|
892
|
+
patchPath: patchFile,
|
|
893
|
+
patchContent: patchText,
|
|
894
|
+
recorded: undoingTop
|
|
895
|
+
? `${target.id}: ${rel}'s editor-authored layer removed; ${nextLayers.length} deviation(s) still recorded`
|
|
896
|
+
: `${target.id}: ${rel} recorded as a patch (${spec.patch})`,
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/** Keep `tracked_file_count` meaning what it means — the manifest's length —
|
|
901
|
+
* and leave it alone when it never meant that in this lock. */
|
|
902
|
+
function syncTrackedCount(before: VendorLock, after: VendorLock): void {
|
|
903
|
+
if (before.tracked_file_count === Object.keys(before.sha256_manifest).length) {
|
|
904
|
+
after.tracked_file_count = Object.keys(after.sha256_manifest).length;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/** The 2-space shape every lock on disk already has, with the trailing newline
|
|
909
|
+
* a text file gets. An emptied `patches` was DELETED by then, so the key
|
|
910
|
+
* disappears from the serialized lock rather than becoming `{}`. */
|
|
911
|
+
function serializeLock(lock: VendorLock): string {
|
|
912
|
+
return `${JSON.stringify(lock, null, 2)}\n`;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// ──────────────────────────────────────────────────────────────── the write
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Write `after` into a vendored game's file AND update the game's lock, as one
|
|
919
|
+
* journalled operation.
|
|
920
|
+
*
|
|
921
|
+
* Every refusal happens before a byte moves — that is the point of planning the
|
|
922
|
+
* whole end state first — and every refusal names what is wrong with the lock
|
|
923
|
+
* or the folder, never a generic failure.
|
|
924
|
+
*/
|
|
925
|
+
export function writeRecordedVendoredFile(
|
|
926
|
+
target: VendoredTarget,
|
|
927
|
+
after: Buffer,
|
|
928
|
+
note?: string,
|
|
929
|
+
): VendoredWriteResult {
|
|
930
|
+
const absFile = join(target.gameDir, ...target.rel.split('/'));
|
|
931
|
+
let plan: Reconciled;
|
|
932
|
+
let current: Buffer;
|
|
933
|
+
try {
|
|
934
|
+
current = readFileSync(absFile);
|
|
935
|
+
plan = reconcileLock(readLock(target.lockPath), target, current, after, note);
|
|
936
|
+
} catch (error) {
|
|
937
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const pending: PendingVendoredWrite = {
|
|
941
|
+
version: 2,
|
|
942
|
+
gameFile: absFile,
|
|
943
|
+
gameFileBase64: after.toString('base64'),
|
|
944
|
+
lockPath: target.lockPath,
|
|
945
|
+
lockContent: serializeLock(plan.lock),
|
|
946
|
+
patchPath: plan.patchPath,
|
|
947
|
+
patchContent: plan.patchContent,
|
|
948
|
+
};
|
|
949
|
+
const stage = hostStateDir(target);
|
|
950
|
+
let journalWritten = false;
|
|
951
|
+
try {
|
|
952
|
+
writeTextThroughRename(journalPathFor(target), JSON.stringify(pending, null, 2), stage);
|
|
953
|
+
journalWritten = true;
|
|
954
|
+
applyPending(pending, stage);
|
|
955
|
+
rmSync(journalPathFor(target), { force: true });
|
|
956
|
+
} catch (error) {
|
|
957
|
+
if (journalWritten) {
|
|
958
|
+
return {
|
|
959
|
+
ok: true,
|
|
960
|
+
recorded:
|
|
961
|
+
`${target.id}: the complete recorder state is committed to its roll-forward journal; ` +
|
|
962
|
+
'the next source access will settle the pending artifacts',
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
966
|
+
}
|
|
967
|
+
return { ok: true, recorded: plan.recorded };
|
|
968
|
+
}
|