@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,1134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE PROJECT WATCHER — the half of the editor server that reacts to the
|
|
3
|
+
* filesystem rather than to a request.
|
|
4
|
+
*
|
|
5
|
+
* Four things, and they are one subsystem because they share the same
|
|
6
|
+
* chokidar streams and the same debounce discipline:
|
|
7
|
+
*
|
|
8
|
+
* - **Asset watching**, with move detection: an unlink followed within
|
|
9
|
+
* `MOVE_WINDOW_MS` by an add of identical bytes is a MOVE, not a delete,
|
|
10
|
+
* and the hash/size cache is what makes that decidable without re-reading
|
|
11
|
+
* the whole tree.
|
|
12
|
+
* - **The component index**, invalidated by `src/` writes and warmed lazily,
|
|
13
|
+
* so the inspector's component list is a live folder scan and never a
|
|
14
|
+
* cached manifest.
|
|
15
|
+
* - **Per-file validation**, whose verdicts reach the terminal, the SSE
|
|
16
|
+
* broadcast and the replayable validation log through ONE formatter, so a
|
|
17
|
+
* replayed line and a live one render identically.
|
|
18
|
+
* - **The build-discipline tripwires**, which ride the save event and the
|
|
19
|
+
* play event and nothing else.
|
|
20
|
+
*
|
|
21
|
+
* It takes a `ProjectWatchHost` rather than the full route context: this runs
|
|
22
|
+
* BEFORE the context exists (the context reads several of its outputs), and
|
|
23
|
+
* the narrow interface is what makes that ordering possible instead of
|
|
24
|
+
* circular. Everything mutable it owns is exposed as an accessor for the same
|
|
25
|
+
* reason the context does it — a project switch reassigns the roots underneath
|
|
26
|
+
* a watcher that is already running.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { type Dirent, existsSync, statSync } from 'node:fs';
|
|
30
|
+
import { readdir } from 'node:fs/promises';
|
|
31
|
+
import { join, relative, resolve, sep } from 'node:path';
|
|
32
|
+
import chokidar, { type FSWatcher } from 'chokidar';
|
|
33
|
+
import { MANIFEST_FILENAME } from '@volter/editor-project/manifest/filename';
|
|
34
|
+
import { resolveManifestPath } from '@volter/editor-project/manifest/locate';
|
|
35
|
+
import {
|
|
36
|
+
advancePersistedTripwireGate,
|
|
37
|
+
commitCadenceBanner,
|
|
38
|
+
commitCadenceGateKey,
|
|
39
|
+
commitCadenceTier,
|
|
40
|
+
IDLE_TRIPWIRE_GATE,
|
|
41
|
+
newestEvidenceMtime,
|
|
42
|
+
readUncommittedWork,
|
|
43
|
+
shouldEvaluateTripwires,
|
|
44
|
+
type TripwireGate,
|
|
45
|
+
unplayedSessionBanner,
|
|
46
|
+
unplayedSessionGateKey,
|
|
47
|
+
unplayedSessionTier,
|
|
48
|
+
} from './support/project/build-discipline';
|
|
49
|
+
import type { SessionJournalEvent } from './support/project/session-journal';
|
|
50
|
+
import type { ProjectComponentEntry } from '../src/asset-workflow/project-content';
|
|
51
|
+
import { ADAPTER_MODULE_FILENAME } from '../src/ui-source/adapter-region-includes';
|
|
52
|
+
import { canonicalProjectRoot } from './canonical-path';
|
|
53
|
+
import type { AgentAuthorLease } from './collaboration-attribution';
|
|
54
|
+
import { filesystemMutationAuthor } from './collaboration-attribution';
|
|
55
|
+
import type { CollaborationSession } from './collaboration-session';
|
|
56
|
+
import type { ConsoleLedger } from './console-ledger';
|
|
57
|
+
import { broadcast } from './editor-sse';
|
|
58
|
+
import { discoverProjectComponents } from './project-components';
|
|
59
|
+
import { collaborationSourceSnapshot, hashAndSize, scanDirForHashes } from './project-file-scan';
|
|
60
|
+
import { classifyValidatableFile, validateProjectFile } from './project-validation';
|
|
61
|
+
import type { SourceValidationState } from './routes/context';
|
|
62
|
+
import {
|
|
63
|
+
classifyProjectSrcPath,
|
|
64
|
+
findUniqueMoveMatch,
|
|
65
|
+
isPathInside,
|
|
66
|
+
projectStorySourceDirs,
|
|
67
|
+
resolveWatcherPollOptions,
|
|
68
|
+
} from './server-utils';
|
|
69
|
+
|
|
70
|
+
/** What the watcher needs from the session it belongs to. */
|
|
71
|
+
export interface ProjectWatchHost {
|
|
72
|
+
readonly engineRoot: string;
|
|
73
|
+
readonly consoleLedger: Pick<ConsoleLedger, 'resolve'>;
|
|
74
|
+
/** Read live — a project switch reassigns both under a running watcher. */
|
|
75
|
+
readonly projectRoot: () => string;
|
|
76
|
+
readonly publicRoot: () => string;
|
|
77
|
+
readonly journalEvent: (event: SessionJournalEvent) => void;
|
|
78
|
+
readonly servingProjectSince: () => number;
|
|
79
|
+
/** The open project's collaboration session, or `null` when none is open. */
|
|
80
|
+
readonly currentCollaboration: () => CollaborationSession | null;
|
|
81
|
+
/** (Re)bind the collaboration session after a project switch. */
|
|
82
|
+
readonly bindCollaboration: () => void;
|
|
83
|
+
/** The live agent-author lease, so a watched write is attributed correctly. */
|
|
84
|
+
readonly recentAgentAuthor: () => AgentAuthorLease | null;
|
|
85
|
+
/** The open project's parsed manifest, or `null` when it cannot be read. */
|
|
86
|
+
readonly readProjectManifest: () => unknown;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface ProjectWatch {
|
|
90
|
+
/** (Re)start every watcher for whatever the roots now are. */
|
|
91
|
+
readonly start: () => void;
|
|
92
|
+
readonly currentProjectComponents: () => Promise<ProjectComponentEntry[]>;
|
|
93
|
+
readonly currentValidationLogEntries: () => { level: 'error' | 'warn'; message: string }[];
|
|
94
|
+
readonly announceBuildDisciplineTripwires: () => void;
|
|
95
|
+
readonly projectValidation: Map<string, { errors: string[]; at: number }>;
|
|
96
|
+
readonly projectWarnings: Map<string, { warnings: string[]; at: number }>;
|
|
97
|
+
readonly expectedEditorMutations: Map<string, { sha: string | null; expiresAt: number }>;
|
|
98
|
+
sourceValidation: SourceValidationState;
|
|
99
|
+
cadenceGate: TripwireGate;
|
|
100
|
+
unplayedGate: TripwireGate;
|
|
101
|
+
publicAssetsLastChangedAt: number | null;
|
|
102
|
+
publicAssetsLastPath: string | null;
|
|
103
|
+
publicAssetsChangedCount: number;
|
|
104
|
+
/** Teardown: stop every stream and clear every pending timer. */
|
|
105
|
+
readonly close: () => Promise<void>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Does a manifest (as read, unvalidated) declare any root — anything `vgai play`
|
|
109
|
+
* could run? A models project declares `roots: []`. */
|
|
110
|
+
function manifestDeclaresRoots(manifest: unknown): boolean {
|
|
111
|
+
const roots = (manifest as { roots?: unknown } | null)?.roots;
|
|
112
|
+
return Array.isArray(roots) && roots.length > 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function createProjectWatch(host: ProjectWatchHost): ProjectWatch {
|
|
116
|
+
const { bindCollaboration, currentCollaboration, engineRoot, journalEvent, readProjectManifest } =
|
|
117
|
+
host;
|
|
118
|
+
|
|
119
|
+
// P20 — the OTHER half of the load clock. `lastIndexRequestAt` (the server's)
|
|
120
|
+
// says when the running document loaded; these say when bytes under
|
|
121
|
+
// `public/` last moved. A write that POSTDATES the load is the divergence
|
|
122
|
+
// `vgai restart` used to paper over: `restart` remounts every root from
|
|
123
|
+
// fresh SOURCE, and re-running module-scope loaders against a page-lifetime
|
|
124
|
+
// asset cache (Pixi `Assets`, three's loader caches) can hand the remounted
|
|
125
|
+
// world the OLD bytes with nothing reporting it. Naming the divergence is
|
|
126
|
+
// all this does — invalidation is not built here (P20 stays open).
|
|
127
|
+
let publicAssetsLastChangedAt: number | null = null;
|
|
128
|
+
let publicAssetsLastPath: string | null = null;
|
|
129
|
+
let publicAssetsChangedCount = 0;
|
|
130
|
+
|
|
131
|
+
// ---- File hash cache + move detection ----
|
|
132
|
+
const fileHashCache = new Map<string, string>();
|
|
133
|
+
const fileSizeCache = new Map<string, number>();
|
|
134
|
+
|
|
135
|
+
interface PendingUnlink {
|
|
136
|
+
absPath: string;
|
|
137
|
+
relativePath: string;
|
|
138
|
+
hash: string;
|
|
139
|
+
size: number;
|
|
140
|
+
timestamp: number;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const MOVE_WINDOW_MS = 3000;
|
|
144
|
+
const pendingUnlinks: PendingUnlink[] = [];
|
|
145
|
+
|
|
146
|
+
// ---- Asset move broadcast ----
|
|
147
|
+
//
|
|
148
|
+
// BROADCAST ONLY. The watcher deliberately rewrites no file itself: asset-ref
|
|
149
|
+
// rewriting for the formats that DO carry references lives in the editor's own
|
|
150
|
+
// `asset-workflow/project-asset-operations.ts`, which routes writes through
|
|
151
|
+
// project history rather than writing behind the editor's back.
|
|
152
|
+
async function handleAssetMove(oldPath: string, newPath: string): Promise<void> {
|
|
153
|
+
broadcast('asset-moved', { oldPath, newPath, updatedFiles: [] });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ---- Chokidar watcher ----
|
|
157
|
+
let watcher: FSWatcher | null = null;
|
|
158
|
+
// An editor-owned atomic write is also observed by chokidar. Match that
|
|
159
|
+
// event by path and resulting digest so it cannot race the authoritative
|
|
160
|
+
// editor mutation into a duplicate, misattributed filesystem revision.
|
|
161
|
+
const expectedEditorMutations = new Map<string, { sha: string | null; expiresAt: number }>();
|
|
162
|
+
let srcWatcher: FSWatcher | null = null;
|
|
163
|
+
let referencesWatcher: FSWatcher | null = null;
|
|
164
|
+
let manifestWatcher: FSWatcher | null = null;
|
|
165
|
+
|
|
166
|
+
// The component index is DERIVED from project source, never a manifest and
|
|
167
|
+
// never persisted. Its first cold scan used to sit directly on the 3D-board
|
|
168
|
+
// gesture (715ms–3.6s in Doctor). Prime it while the editor is booting and
|
|
169
|
+
// invalidate from the same source/declaration watchers that define its
|
|
170
|
+
// inputs. A generation check prevents a request whose scan overlapped a
|
|
171
|
+
// save from publishing the pre-save answer.
|
|
172
|
+
let componentIndexGeneration = 0;
|
|
173
|
+
let componentIndexCache: {
|
|
174
|
+
readonly root: string;
|
|
175
|
+
readonly generation: number;
|
|
176
|
+
readonly promise: ReturnType<typeof discoverProjectComponents>;
|
|
177
|
+
} | null = null;
|
|
178
|
+
let componentIndexWarmTimer: ReturnType<typeof setTimeout> | null = null;
|
|
179
|
+
function invalidateComponentIndex(): void {
|
|
180
|
+
componentIndexGeneration += 1;
|
|
181
|
+
componentIndexCache = null;
|
|
182
|
+
if (componentIndexWarmTimer) clearTimeout(componentIndexWarmTimer);
|
|
183
|
+
if (host.projectRoot() === engineRoot) return;
|
|
184
|
+
// Source saves frequently arrive as a short chokidar burst. Re-prime once
|
|
185
|
+
// at its trailing edge so the NEXT gallery/board gesture does not inherit
|
|
186
|
+
// the rescan, while never running one scan per low-level event.
|
|
187
|
+
componentIndexWarmTimer = setTimeout(() => {
|
|
188
|
+
componentIndexWarmTimer = null;
|
|
189
|
+
void currentProjectComponents().catch(() => {});
|
|
190
|
+
}, 100);
|
|
191
|
+
}
|
|
192
|
+
async function currentProjectComponents(): Promise<
|
|
193
|
+
Awaited<ReturnType<typeof discoverProjectComponents>>
|
|
194
|
+
> {
|
|
195
|
+
if (host.projectRoot() === engineRoot) return [];
|
|
196
|
+
const generation = componentIndexGeneration;
|
|
197
|
+
const cached = componentIndexCache;
|
|
198
|
+
const promise =
|
|
199
|
+
cached && cached.root === host.projectRoot() && cached.generation === generation
|
|
200
|
+
? cached.promise
|
|
201
|
+
: discoverProjectComponents(host.projectRoot());
|
|
202
|
+
if (promise !== cached?.promise) {
|
|
203
|
+
componentIndexCache = { root: host.projectRoot(), generation, promise };
|
|
204
|
+
}
|
|
205
|
+
let components: Awaited<ReturnType<typeof discoverProjectComponents>>;
|
|
206
|
+
try {
|
|
207
|
+
components = await promise;
|
|
208
|
+
} catch (error) {
|
|
209
|
+
// A failed cold scan is not a durable cache entry. The warm-up caller
|
|
210
|
+
// deliberately ignores its failure; the first real request retries and
|
|
211
|
+
// reports that result instead of inheriting a rejected promise forever.
|
|
212
|
+
if (componentIndexCache?.promise === promise) componentIndexCache = null;
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
if (
|
|
216
|
+
generation !== componentIndexGeneration ||
|
|
217
|
+
componentIndexCache?.root !== host.projectRoot()
|
|
218
|
+
) {
|
|
219
|
+
return currentProjectComponents();
|
|
220
|
+
}
|
|
221
|
+
return components;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Coalesce event bursts (e.g. a git checkout adding several files at once)
|
|
225
|
+
// into one broadcast per event name, trailing ~250ms after the last event.
|
|
226
|
+
const pendingBroadcasts = new Map<string, ReturnType<typeof setTimeout>>();
|
|
227
|
+
function broadcastDebounced(event: string, data: unknown, ms = 250): void {
|
|
228
|
+
const existing = pendingBroadcasts.get(event);
|
|
229
|
+
if (existing) clearTimeout(existing);
|
|
230
|
+
pendingBroadcasts.set(
|
|
231
|
+
event,
|
|
232
|
+
setTimeout(() => {
|
|
233
|
+
pendingBroadcasts.delete(event);
|
|
234
|
+
broadcast(event, data);
|
|
235
|
+
}, ms),
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ---- Validate-on-change (owner decision, GitHub #103) ----
|
|
240
|
+
//
|
|
241
|
+
// Hand-editing project files is an accepted authoring path — the
|
|
242
|
+
// requirement is that invalid output surfaces IMMEDIATELY and OBVIOUSLY
|
|
243
|
+
// instead of only erroring at next load. `project-validation.ts` owns the
|
|
244
|
+
// content check (reusing the engine's own Zod parsers, same ones
|
|
245
|
+
// validate-scenes.ts/validate-manifest.ts run); this section owns
|
|
246
|
+
// debouncing + the three report surfaces: the terminal, connected editor
|
|
247
|
+
// tabs (the existing `server-log` SSE event `editor-console.ts` already
|
|
248
|
+
// listens for — previously unused, since nothing ever broadcast it), and
|
|
249
|
+
// `/__editor/state`'s `projectValidation` field.
|
|
250
|
+
//
|
|
251
|
+
// A file is present in `projectValidation` ONLY while it is currently
|
|
252
|
+
// failing — a valid write removes its entry entirely ("ok" is represented
|
|
253
|
+
// by absence, not a stored value), which is what keeps the happy path
|
|
254
|
+
// silent both on the wire and in the terminal (rule 4: editor-originated
|
|
255
|
+
// autosaves validate clean by construction and must produce zero noise).
|
|
256
|
+
interface FileValidationFailure {
|
|
257
|
+
errors: string[];
|
|
258
|
+
at: number;
|
|
259
|
+
}
|
|
260
|
+
interface FileValidationWarnings {
|
|
261
|
+
warnings: string[];
|
|
262
|
+
at: number;
|
|
263
|
+
}
|
|
264
|
+
const projectValidation = new Map<string, FileValidationFailure>();
|
|
265
|
+
const projectWarnings = new Map<string, FileValidationWarnings>();
|
|
266
|
+
// PD-14: whether source validation (the `<project>/src` watcher and the
|
|
267
|
+
// boot-equivalent tree scan behind it) is actually running.
|
|
268
|
+
//
|
|
269
|
+
// `'awaiting-src'` is the honest name for a project with no `src/` yet: the
|
|
270
|
+
// watcher is armed on the not-yet-existing path and starts by itself when
|
|
271
|
+
// the directory appears, but until then NOTHING under `src/` is checked.
|
|
272
|
+
// That state used to be permanent AND silent — the source watcher was gated
|
|
273
|
+
// on a single `existsSync()` at boot, so a project scaffolded into an
|
|
274
|
+
// already-open editor (or one whose `src/` was deleted and recreated) was
|
|
275
|
+
// never source-validated again for the server's whole lifetime, and every
|
|
276
|
+
// surface reported a clean project that had simply never been looked at.
|
|
277
|
+
// Degrade loudly: the terminal, the editor console (via the
|
|
278
|
+
// `/__editor/validation-log` replay) and the status payload all say so.
|
|
279
|
+
let sourceValidation: SourceValidationState = 'no-project';
|
|
280
|
+
/**
|
|
281
|
+
* The directories source validation covers — `projectStorySourceDirs` over
|
|
282
|
+
* the open manifest, set by `startWatcher` and read by the warning below.
|
|
283
|
+
*
|
|
284
|
+
* NOT `<project>/src` alone. A repo-vendored game's manifest declares a root
|
|
285
|
+
* whose `entry` resolves OUTSIDE the project folder
|
|
286
|
+
* (`packages/editor/src/ingest/games/<id>/` holds the manifest and the host
|
|
287
|
+
* shim; the game's source is `vendor/games/<id>/src/`), and the
|
|
288
|
+
* ingest-authoring model writes THAT tree. Keying the scan on
|
|
289
|
+
* `<project>/src` meant every such project reported "source validation is
|
|
290
|
+
* NOT running" forever while the watcher beside it — already derived from
|
|
291
|
+
* the manifest since it had the same bug — was watching the real source
|
|
292
|
+
* tree the whole time. The two halves now read one set.
|
|
293
|
+
*/
|
|
294
|
+
let sourceValidationDirs: string[] = [join(host.projectRoot(), 'src')];
|
|
295
|
+
const validationDebounceTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
296
|
+
const VALIDATION_DEBOUNCE_MS = 150;
|
|
297
|
+
const MAX_PRINTED_ERRORS = 5;
|
|
298
|
+
|
|
299
|
+
/** Project-relative, forward-slash path — the key `projectValidation` and
|
|
300
|
+
* the terminal/SSE messages use. */
|
|
301
|
+
function relKey(absPath: string): string {
|
|
302
|
+
return relative(host.projectRoot(), absPath).split(sep).join('/');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function printValidationFailure(rel: string, errors: string[]): void {
|
|
306
|
+
const shown = errors.slice(0, MAX_PRINTED_ERRORS);
|
|
307
|
+
const rest = errors.length - shown.length;
|
|
308
|
+
console.error(
|
|
309
|
+
`\n \x1b[31m\x1b[1m✖ Invalid project file\x1b[0m \x1b[36m${rel}\x1b[0m\n` +
|
|
310
|
+
shown.map((e) => ` \x1b[31m•\x1b[0m ${e}`).join('\n') +
|
|
311
|
+
(rest > 0 ? `\n … and ${rest} more issue(s)` : '') +
|
|
312
|
+
'\n',
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function printValidationRecovery(rel: string): void {
|
|
317
|
+
console.log(` \x1b[32m✓ ${rel} is valid again\x1b[0m`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function validationMessage(rel: string, errors: string[]): string {
|
|
321
|
+
const shown = errors.slice(0, MAX_PRINTED_ERRORS);
|
|
322
|
+
const rest = errors.length - shown.length;
|
|
323
|
+
return (
|
|
324
|
+
`Invalid project file ${rel}:\n` +
|
|
325
|
+
shown.map((e) => ` - ${e}`).join('\n') +
|
|
326
|
+
(rest > 0 ? `\n … and ${rest} more issue(s)` : '')
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function warningMessage(rel: string, warnings: string[]): string {
|
|
331
|
+
const shown = warnings.slice(0, MAX_PRINTED_ERRORS);
|
|
332
|
+
const rest = warnings.length - shown.length;
|
|
333
|
+
return (
|
|
334
|
+
`R3F authoring conventions in ${rel}:\n` +
|
|
335
|
+
shown.map((warning) => ` - ${warning}`).join('\n') +
|
|
336
|
+
(rest > 0 ? `\n … and ${rest} more warning(s)` : '')
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Every directory the scan covers that is not on disk — the reason
|
|
341
|
+
* validation is not running, named rather than assumed. */
|
|
342
|
+
function missingSourceDirs(): string[] {
|
|
343
|
+
return sourceValidationDirs.filter((dir) => !existsSync(dir));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** PD-14: the one wording for "nothing under the project's source tree is
|
|
347
|
+
* being validated", shared by the terminal line, the live `server-log`
|
|
348
|
+
* broadcast and the `/__editor/validation-log` replay so all three read
|
|
349
|
+
* identically. It names EVERY declared source dir (see
|
|
350
|
+
* `sourceValidationDirs`), because for a repo-vendored game the one that
|
|
351
|
+
* matters is not `<project>/src`. */
|
|
352
|
+
function sourceValidationWarningMessage(): string {
|
|
353
|
+
const missing = missingSourceDirs();
|
|
354
|
+
return (
|
|
355
|
+
`Source validation is NOT running: ${missing.join(', ') || join(host.projectRoot(), 'src')} ` +
|
|
356
|
+
`${missing.length === 1 ? 'does' : 'do'} not exist.\n` +
|
|
357
|
+
" Nothing under this project's source tree is being checked — the watcher " +
|
|
358
|
+
'starts by itself the moment the directory appears (no editor restart needed).'
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** Announce a change in whether source validation is running. Silent when
|
|
363
|
+
* nothing changed, and silent for the happy path (a project that has
|
|
364
|
+
* `src/` at boot) — same rule the per-file pass follows. */
|
|
365
|
+
function setSourceValidation(next: SourceValidationState): void {
|
|
366
|
+
const previous = sourceValidation;
|
|
367
|
+
if (previous === next) return;
|
|
368
|
+
sourceValidation = next;
|
|
369
|
+
if (next === 'awaiting-src') {
|
|
370
|
+
const message = sourceValidationWarningMessage();
|
|
371
|
+
console.warn(
|
|
372
|
+
`\n \x1b[33m\x1b[1m⚠ Source validation is not running\x1b[0m\n ${message.split('\n').join('\n ')}\n`,
|
|
373
|
+
);
|
|
374
|
+
broadcast('server-log', { level: 'warn', message });
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (next === 'active' && previous === 'awaiting-src') {
|
|
378
|
+
const message = `${sourceValidationDirs.filter((dir) => existsSync(dir)).join(', ')} appeared — source validation is running`;
|
|
379
|
+
console.log(` \x1b[32m✓ ${message}\x1b[0m`);
|
|
380
|
+
broadcast('server-log', { level: 'info', message });
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Every currently-known validation failure and authoring warning, shaped as
|
|
386
|
+
* `server-log` payloads — the same `{ level, message }` the live broadcasts
|
|
387
|
+
* below emit, so the editor console renders a replayed line identically to
|
|
388
|
+
* one it received as it happened.
|
|
389
|
+
*
|
|
390
|
+
* PD-13: this exists because the live broadcast is the ONLY thing that ever
|
|
391
|
+
* fed the editor console, and both maps are routinely populated before any
|
|
392
|
+
* tab is connected (the boot scan) or re-populated with an unchanged set (no
|
|
393
|
+
* re-broadcast, by design — see `runFileValidation`). Reading the maps at
|
|
394
|
+
* connect time is what makes the console surface state rather than history.
|
|
395
|
+
*/
|
|
396
|
+
function currentValidationLogEntries(): { level: 'error' | 'warn'; message: string }[] {
|
|
397
|
+
const entries: { level: 'error' | 'warn'; message: string }[] = [];
|
|
398
|
+
// PD-14: first, because it qualifies everything below it — an empty
|
|
399
|
+
// failure list means nothing while the source pass is not running.
|
|
400
|
+
if (sourceValidation === 'awaiting-src') {
|
|
401
|
+
entries.push({ level: 'warn', message: sourceValidationWarningMessage() });
|
|
402
|
+
}
|
|
403
|
+
for (const [rel, failure] of projectValidation) {
|
|
404
|
+
entries.push({ level: 'error', message: validationMessage(rel, failure.errors) });
|
|
405
|
+
}
|
|
406
|
+
for (const [rel, warned] of projectWarnings) {
|
|
407
|
+
entries.push({ level: 'warn', message: warningMessage(rel, warned.warnings) });
|
|
408
|
+
}
|
|
409
|
+
return entries;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ---- Build-discipline tripwires on the save event (P53) ----
|
|
413
|
+
//
|
|
414
|
+
// "This work has been uncommitted for 40 minutes" and "this session has
|
|
415
|
+
// never once played the game" used to reach an agent through `vgai status`
|
|
416
|
+
// and nowhere else. Measured on a blind probe: a 17-minute build ran the
|
|
417
|
+
// editor, `playtest` and `eval` and invoked `vgai status` ZERO times, so
|
|
418
|
+
// neither banner had a delivery path and the build still landed as one
|
|
419
|
+
// end-of-run commit. The mechanisms were right; "a building agent polls
|
|
420
|
+
// status constantly" was false.
|
|
421
|
+
//
|
|
422
|
+
// This terminal is the one an agent DOES watch — it is where save-validation
|
|
423
|
+
// failures appear — so the same banners ride the save event that is already
|
|
424
|
+
// being handled a few lines below. Deliberately not a timer and not a new
|
|
425
|
+
// watcher: no event, no reads. `@vgai/sdk/build-discipline` owns every word
|
|
426
|
+
// and every threshold; this is only a channel.
|
|
427
|
+
let cadenceGate: TripwireGate = IDLE_TRIPWIRE_GATE;
|
|
428
|
+
let unplayedGate: TripwireGate = IDLE_TRIPWIRE_GATE;
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Report whichever tripwire just CROSSED a step, at most once per crossing.
|
|
432
|
+
*
|
|
433
|
+
* Two bounds, both from `build-discipline.ts` and both needed because a
|
|
434
|
+
* "save" here means a chokidar event and an agent's write wave is dozens of
|
|
435
|
+
* them a second: `shouldEvaluateTripwires` caps how often the git/stat reads
|
|
436
|
+
* run at all, and the gate prints only when the measured step is higher than
|
|
437
|
+
* the highest one already printed (so a burst prints once, the escalation
|
|
438
|
+
* prints again, and a commit or a play re-arms it).
|
|
439
|
+
*
|
|
440
|
+
* The gate is `advancePersistedTripwireGate`, not the in-memory fold: this
|
|
441
|
+
* process restarts far more often than a dirty batch resolves, and a gate
|
|
442
|
+
* that lived only here re-armed at "nothing announced" on every restart — a
|
|
443
|
+
* measured 45-minute build was NOTICED three times and never once escalated
|
|
444
|
+
* to the loud step. See that function's header for the whole story.
|
|
445
|
+
*/
|
|
446
|
+
function announceBuildDisciplineTripwires(): void {
|
|
447
|
+
if (host.projectRoot() === engineRoot) return;
|
|
448
|
+
const now = Date.now();
|
|
449
|
+
// The two gates advance together, so either one answers "is a read due?".
|
|
450
|
+
if (!shouldEvaluateTripwires(cadenceGate, now)) return;
|
|
451
|
+
try {
|
|
452
|
+
const work = readUncommittedWork(host.projectRoot(), now);
|
|
453
|
+
const cadenceTier = commitCadenceTier(work);
|
|
454
|
+
const cadenceStep = advancePersistedTripwireGate(
|
|
455
|
+
host.projectRoot(),
|
|
456
|
+
'commit-cadence',
|
|
457
|
+
commitCadenceGateKey(host.projectRoot()),
|
|
458
|
+
cadenceGate,
|
|
459
|
+
cadenceTier,
|
|
460
|
+
now,
|
|
461
|
+
);
|
|
462
|
+
cadenceGate = cadenceStep.gate;
|
|
463
|
+
if (cadenceStep.announce) {
|
|
464
|
+
// Journal the CROSSING as data first, then render the banner — the
|
|
465
|
+
// banner is prose an agent may never look at, the line is the fact.
|
|
466
|
+
journalEvent({
|
|
467
|
+
kind: 'tripwire',
|
|
468
|
+
tripwire: 'commit-cadence',
|
|
469
|
+
tier: cadenceTier,
|
|
470
|
+
ageMs: work?.ageMs ?? 0,
|
|
471
|
+
fileCount: work?.fileCount ?? 0,
|
|
472
|
+
});
|
|
473
|
+
const banner = commitCadenceBanner(work);
|
|
474
|
+
if (banner) console.warn(`\n${banner}\n`);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const evidence = newestEvidenceMtime(host.projectRoot());
|
|
478
|
+
const playable = manifestDeclaresRoots(readProjectManifest());
|
|
479
|
+
const unplayedTier = unplayedSessionTier(host.servingProjectSince(), evidence, now, playable);
|
|
480
|
+
const unplayedStep = advancePersistedTripwireGate(
|
|
481
|
+
host.projectRoot(),
|
|
482
|
+
'unplayed-session',
|
|
483
|
+
unplayedSessionGateKey(evidence),
|
|
484
|
+
unplayedGate,
|
|
485
|
+
unplayedTier,
|
|
486
|
+
now,
|
|
487
|
+
);
|
|
488
|
+
unplayedGate = unplayedStep.gate;
|
|
489
|
+
if (unplayedStep.announce) {
|
|
490
|
+
journalEvent({
|
|
491
|
+
kind: 'tripwire',
|
|
492
|
+
tripwire: 'unplayed-session',
|
|
493
|
+
tier: unplayedTier,
|
|
494
|
+
servingForMs: now - host.servingProjectSince(),
|
|
495
|
+
});
|
|
496
|
+
const banner = unplayedSessionBanner(host.servingProjectSince(), evidence, now, playable);
|
|
497
|
+
if (banner) console.warn(`\n${banner}\n`);
|
|
498
|
+
}
|
|
499
|
+
} catch {
|
|
500
|
+
// A tripwire that breaks a save is worse than a tripwire that misses one.
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* The ONE emit point for a save-validation verdict.
|
|
506
|
+
*
|
|
507
|
+
* It appends the journal line and THEN renders it to the two live surfaces
|
|
508
|
+
* (this terminal, the editor console). Nothing prints a verdict anywhere
|
|
509
|
+
* else, which is what makes "no event prints without journaling" structural
|
|
510
|
+
* rather than a convention: a renderer that forgot the journal would have to
|
|
511
|
+
* be a second copy of this function.
|
|
512
|
+
*
|
|
513
|
+
* Only the surface-worthy TRANSITIONS pass through here — the same set that
|
|
514
|
+
* already printed. A clean save over an already-clean file stays silent in
|
|
515
|
+
* the journal too (the happy-path silence rule), so the file an agent reads
|
|
516
|
+
* is a record of what changed, not a keystroke log.
|
|
517
|
+
*/
|
|
518
|
+
function emitValidationVerdict(
|
|
519
|
+
rel: string,
|
|
520
|
+
fileKind: string,
|
|
521
|
+
verdict:
|
|
522
|
+
| { outcome: 'error'; errors: string[] }
|
|
523
|
+
| { outcome: 'recovered' }
|
|
524
|
+
| { outcome: 'warning'; warnings: string[] },
|
|
525
|
+
): void {
|
|
526
|
+
if (verdict.outcome === 'error') {
|
|
527
|
+
journalEvent({
|
|
528
|
+
kind: 'validation',
|
|
529
|
+
path: rel,
|
|
530
|
+
fileKind,
|
|
531
|
+
ok: false,
|
|
532
|
+
errors: verdict.errors,
|
|
533
|
+
});
|
|
534
|
+
printValidationFailure(rel, verdict.errors);
|
|
535
|
+
broadcast('server-log', { level: 'error', message: validationMessage(rel, verdict.errors) });
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (verdict.outcome === 'recovered') {
|
|
539
|
+
journalEvent({ kind: 'validation', path: rel, fileKind, ok: true });
|
|
540
|
+
printValidationRecovery(rel);
|
|
541
|
+
broadcast('server-log', { level: 'info', message: `${rel} is valid again` });
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
journalEvent({
|
|
545
|
+
kind: 'validation',
|
|
546
|
+
path: rel,
|
|
547
|
+
fileKind,
|
|
548
|
+
ok: true,
|
|
549
|
+
warnings: verdict.warnings,
|
|
550
|
+
});
|
|
551
|
+
const message = warningMessage(rel, verdict.warnings);
|
|
552
|
+
console.warn(
|
|
553
|
+
`\n \x1b[33m\x1b[1m⚠ R3F authoring warnings\x1b[0m \x1b[36m${rel}\x1b[0m\n${message.split('\n').slice(1).join('\n')}\n`,
|
|
554
|
+
);
|
|
555
|
+
broadcast('server-log', { level: 'warn', message });
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** Run (already-debounced) validation for one absolute path and report the
|
|
559
|
+
* result on all three surfaces. Non-validatable paths are a silent no-op
|
|
560
|
+
* so callers can invoke this unconditionally from watcher events. */
|
|
561
|
+
async function runFileValidation(absPath: string): Promise<void> {
|
|
562
|
+
const kind = classifyValidatableFile(absPath);
|
|
563
|
+
if (!kind) return;
|
|
564
|
+
// A project file just landed — the event the tripwires ride. Before the
|
|
565
|
+
// validation result, because it must fire whether the save was valid or
|
|
566
|
+
// not: a broken save is not a reason to go quiet about the last 40
|
|
567
|
+
// minutes.
|
|
568
|
+
announceBuildDisciplineTripwires();
|
|
569
|
+
const rel = relKey(absPath);
|
|
570
|
+
const result = await validateProjectFile(absPath, kind, { engineRoot });
|
|
571
|
+
const previousErrors = projectValidation.get(rel)?.errors ?? [];
|
|
572
|
+
const wasFailing = previousErrors.length > 0;
|
|
573
|
+
const previousWarnings = projectWarnings.get(rel)?.warnings ?? [];
|
|
574
|
+
const warnings = result.warnings ?? [];
|
|
575
|
+
if (previousWarnings.length && JSON.stringify(previousWarnings) !== JSON.stringify(warnings)) {
|
|
576
|
+
host.consoleLedger.resolve(
|
|
577
|
+
[{ severity: 'warn', message: warningMessage(rel, previousWarnings) }],
|
|
578
|
+
'project-validation',
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
if (
|
|
582
|
+
previousErrors.length &&
|
|
583
|
+
(result.ok || JSON.stringify(previousErrors) !== JSON.stringify(result.errors))
|
|
584
|
+
) {
|
|
585
|
+
host.consoleLedger.resolve(
|
|
586
|
+
[{ severity: 'error', message: validationMessage(rel, previousErrors) }],
|
|
587
|
+
'project-validation',
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
if (warnings.length === 0) {
|
|
591
|
+
projectWarnings.delete(rel);
|
|
592
|
+
} else {
|
|
593
|
+
projectWarnings.set(rel, { warnings, at: Date.now() });
|
|
594
|
+
if (JSON.stringify(previousWarnings) !== JSON.stringify(warnings)) {
|
|
595
|
+
emitValidationVerdict(rel, kind, { outcome: 'warning', warnings });
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (result.ok) {
|
|
600
|
+
if (wasFailing) {
|
|
601
|
+
projectValidation.delete(rel);
|
|
602
|
+
emitValidationVerdict(rel, kind, { outcome: 'recovered' });
|
|
603
|
+
}
|
|
604
|
+
// Clean write, never previously failing — the happy-path silence rule:
|
|
605
|
+
// no terminal line, no broadcast, no state entry.
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
projectValidation.set(rel, { errors: result.errors, at: Date.now() });
|
|
610
|
+
emitValidationVerdict(rel, kind, { outcome: 'error', errors: result.errors });
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** Debounce per absolute file path (~150ms) so a burst of writes to the
|
|
614
|
+
* same file (editors/OSes routinely write-then-touch, or write in two
|
|
615
|
+
* chunks) triggers exactly one validation pass. */
|
|
616
|
+
function scheduleFileValidation(absPath: string): void {
|
|
617
|
+
if (!classifyValidatableFile(absPath)) return;
|
|
618
|
+
const existing = validationDebounceTimers.get(absPath);
|
|
619
|
+
if (existing) clearTimeout(existing);
|
|
620
|
+
validationDebounceTimers.set(
|
|
621
|
+
absPath,
|
|
622
|
+
setTimeout(() => {
|
|
623
|
+
validationDebounceTimers.delete(absPath);
|
|
624
|
+
void runFileValidation(absPath);
|
|
625
|
+
}, VALIDATION_DEBOUNCE_MS),
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/** A deleted file can no longer be "invalid" — drop any stale entry/timer
|
|
630
|
+
* so `projectValidation` doesn't keep reporting a ghost error. */
|
|
631
|
+
function clearFileValidation(absPath: string): void {
|
|
632
|
+
const timer = validationDebounceTimers.get(absPath);
|
|
633
|
+
if (timer) {
|
|
634
|
+
clearTimeout(timer);
|
|
635
|
+
validationDebounceTimers.delete(absPath);
|
|
636
|
+
}
|
|
637
|
+
const rel = relKey(absPath);
|
|
638
|
+
const errors = projectValidation.get(rel)?.errors;
|
|
639
|
+
const warnings = projectWarnings.get(rel)?.warnings;
|
|
640
|
+
if (errors)
|
|
641
|
+
host.consoleLedger.resolve(
|
|
642
|
+
[{ severity: 'error', message: validationMessage(rel, errors) }],
|
|
643
|
+
'project-validation',
|
|
644
|
+
);
|
|
645
|
+
if (warnings)
|
|
646
|
+
host.consoleLedger.resolve(
|
|
647
|
+
[{ severity: 'warn', message: warningMessage(rel, warnings) }],
|
|
648
|
+
'project-validation',
|
|
649
|
+
);
|
|
650
|
+
projectValidation.delete(rel);
|
|
651
|
+
projectWarnings.delete(rel);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
async function validateSourceTree(dir: string): Promise<void> {
|
|
655
|
+
let entries: Dirent[];
|
|
656
|
+
try {
|
|
657
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
658
|
+
} catch {
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
await Promise.all(
|
|
662
|
+
entries
|
|
663
|
+
.filter((entry) => !entry.name.startsWith('.'))
|
|
664
|
+
.map(async (entry) => {
|
|
665
|
+
const full = join(dir, entry.name);
|
|
666
|
+
if (entry.isDirectory()) return validateSourceTree(full);
|
|
667
|
+
if (entry.isFile()) return runFileValidation(full);
|
|
668
|
+
}),
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function startWatcher(): void {
|
|
673
|
+
watcher?.close();
|
|
674
|
+
srcWatcher?.close();
|
|
675
|
+
referencesWatcher?.close();
|
|
676
|
+
manifestWatcher?.close();
|
|
677
|
+
srcWatcher = null;
|
|
678
|
+
referencesWatcher = null;
|
|
679
|
+
manifestWatcher = null;
|
|
680
|
+
fileHashCache.clear();
|
|
681
|
+
fileSizeCache.clear();
|
|
682
|
+
pendingUnlinks.length = 0;
|
|
683
|
+
invalidateComponentIndex();
|
|
684
|
+
// Validation state is project-scoped — a previous project's failures
|
|
685
|
+
// must not leak into (or block reporting for) the newly opened one.
|
|
686
|
+
for (const timer of validationDebounceTimers.values()) clearTimeout(timer);
|
|
687
|
+
validationDebounceTimers.clear();
|
|
688
|
+
projectValidation.clear();
|
|
689
|
+
projectWarnings.clear();
|
|
690
|
+
// PD-14: reset before the src watcher re-decides below, so the newly
|
|
691
|
+
// opened project ANNOUNCES its own state (a src-less project switched to
|
|
692
|
+
// from a healthy one must warn) rather than inheriting the previous
|
|
693
|
+
// project's — and so a project that has `src/` stays silent either way.
|
|
694
|
+
sourceValidation = 'no-project';
|
|
695
|
+
// …and the SET it is a verdict about, so a project opened before the src
|
|
696
|
+
// watcher re-derives below cannot be described by the previous one's dirs.
|
|
697
|
+
sourceValidationDirs = [join(host.projectRoot(), 'src')];
|
|
698
|
+
bindCollaboration();
|
|
699
|
+
const snapshotRoot = host.projectRoot();
|
|
700
|
+
const snapshotSession = currentCollaboration();
|
|
701
|
+
const snapshotRevision = snapshotSession?.snapshot().revision ?? 0;
|
|
702
|
+
if (snapshotSession) {
|
|
703
|
+
void collaborationSourceSnapshot(snapshotRoot)
|
|
704
|
+
.then((resources) => {
|
|
705
|
+
if (canonicalProjectRoot(host.projectRoot()) !== canonicalProjectRoot(snapshotRoot))
|
|
706
|
+
return;
|
|
707
|
+
// The initial scan is only a baseline. If an editor write or watcher
|
|
708
|
+
// revision landed while the filesystem walk was in flight, applying
|
|
709
|
+
// its later view would race that attributed mutation and steal its
|
|
710
|
+
// authorship as a generic filesystem change.
|
|
711
|
+
if (snapshotSession.snapshot().revision !== snapshotRevision) return;
|
|
712
|
+
snapshotSession.synchronizeSourceSnapshot('host', resources);
|
|
713
|
+
})
|
|
714
|
+
.catch((error) => {
|
|
715
|
+
broadcast('server-log', {
|
|
716
|
+
level: 'error',
|
|
717
|
+
message: `Collaboration source baseline failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
718
|
+
});
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const recordFilesystemMutation = async (
|
|
723
|
+
absPath: string,
|
|
724
|
+
knownSha?: string | null,
|
|
725
|
+
): Promise<void> => {
|
|
726
|
+
const collaboration = currentCollaboration();
|
|
727
|
+
const abs = resolve(absPath);
|
|
728
|
+
if (!collaboration || !isPathInside(host.projectRoot(), abs)) return;
|
|
729
|
+
try {
|
|
730
|
+
const sha = knownSha === undefined ? (await hashAndSize(abs)).hash : knownSha;
|
|
731
|
+
const relativePath = relative(host.projectRoot(), abs).split(sep).join('/');
|
|
732
|
+
const expected = expectedEditorMutations.get(relativePath);
|
|
733
|
+
if (expected) {
|
|
734
|
+
if (expected.expiresAt >= Date.now() && expected.sha === sha) return;
|
|
735
|
+
expectedEditorMutations.delete(relativePath);
|
|
736
|
+
}
|
|
737
|
+
collaboration.recordSourceMutation({
|
|
738
|
+
authorId: filesystemMutationAuthor(host.recentAgentAuthor()),
|
|
739
|
+
source: 'filesystem',
|
|
740
|
+
resources: [{ path: relativePath, sha }],
|
|
741
|
+
});
|
|
742
|
+
} catch {
|
|
743
|
+
// A write may be replaced again before chokidar's event is handled;
|
|
744
|
+
// the subsequent event records the bytes that actually survived.
|
|
745
|
+
}
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
// Seed hash cache in background
|
|
749
|
+
scanDirForHashes(host.publicRoot(), fileHashCache, fileSizeCache);
|
|
750
|
+
|
|
751
|
+
// #131: on drvfs (WSL /mnt/<drive>) inotify never fires, so without
|
|
752
|
+
// polling every broadcast below is dead and the editor UI sits stale
|
|
753
|
+
// until a manual refresh. Auto-detection keys off the PROJECT root and is
|
|
754
|
+
// skipped in engine-repo mode (host.projectRoot() === engineRoot), where
|
|
755
|
+
// `public/` includes the vendored-game trees — `VGAI_WATCH_POLL` still
|
|
756
|
+
// forces it there. Re-derived on every startWatcher() call so a project
|
|
757
|
+
// switch onto/off a drvfs mount re-decides correctly.
|
|
758
|
+
const pollOptions = resolveWatcherPollOptions(
|
|
759
|
+
host.projectRoot() === engineRoot ? null : host.projectRoot(),
|
|
760
|
+
);
|
|
761
|
+
|
|
762
|
+
watcher = chokidar.watch(host.publicRoot(), {
|
|
763
|
+
ignoreInitial: true,
|
|
764
|
+
ignored: /(^|[/\\])\../, // ignore dotfiles
|
|
765
|
+
...pollOptions,
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
const broadcastPublicAssetChanged = (absPath: string): void => {
|
|
769
|
+
const path = relative(host.publicRoot(), absPath).split(sep).join('/');
|
|
770
|
+
// P20 — the session's own record that bytes under `public/` MOVED, and
|
|
771
|
+
// when. This watcher is the door rather than the project-outputs writer
|
|
772
|
+
// (`project-output-writer.ts`) deliberately: the writer sees only the
|
|
773
|
+
// batches that go through it, while this sees the bytes actually
|
|
774
|
+
// landing — a bake through the outputs door, a `vgai add` copy, and a
|
|
775
|
+
// hand-written file all stamp the same way, because what matters
|
|
776
|
+
// downstream is that the running document's caches predate them.
|
|
777
|
+
publicAssetsLastChangedAt = Date.now();
|
|
778
|
+
publicAssetsLastPath = path;
|
|
779
|
+
publicAssetsChangedCount += 1;
|
|
780
|
+
broadcast('assets-changed', { paths: [path] });
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
watcher.on('change', async (filePath: string) => {
|
|
784
|
+
const abs = resolve(filePath);
|
|
785
|
+
if (!isPathInside(host.publicRoot(), abs)) return;
|
|
786
|
+
try {
|
|
787
|
+
const { hash, size } = await hashAndSize(abs);
|
|
788
|
+
fileHashCache.set(abs, hash);
|
|
789
|
+
fileSizeCache.set(abs, size);
|
|
790
|
+
void recordFilesystemMutation(abs, hash);
|
|
791
|
+
} catch {
|
|
792
|
+
/* ignore */
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Validate-on-change (#103): a hand-edited (or autosaved) document change
|
|
796
|
+
// gets checked against the engine's own Zod parsers — see the
|
|
797
|
+
// "Validate-on-change" section above and `classifyValidatableFile`.
|
|
798
|
+
scheduleFileValidation(abs);
|
|
799
|
+
broadcastPublicAssetChanged(abs);
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
watcher.on('unlink', (filePath: string) => {
|
|
803
|
+
const abs = resolve(filePath);
|
|
804
|
+
if (!isPathInside(host.publicRoot(), abs)) return;
|
|
805
|
+
clearFileValidation(abs);
|
|
806
|
+
void recordFilesystemMutation(abs, null);
|
|
807
|
+
broadcastPublicAssetChanged(abs);
|
|
808
|
+
const hash = fileHashCache.get(abs);
|
|
809
|
+
const size = fileSizeCache.get(abs);
|
|
810
|
+
if (hash !== undefined && size !== undefined) {
|
|
811
|
+
const rel = `/${relative(host.publicRoot(), abs)}`;
|
|
812
|
+
pendingUnlinks.push({ absPath: abs, relativePath: rel, hash, size, timestamp: Date.now() });
|
|
813
|
+
fileHashCache.delete(abs);
|
|
814
|
+
fileSizeCache.delete(abs);
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
watcher.on('add', async (filePath: string) => {
|
|
819
|
+
const abs = resolve(filePath);
|
|
820
|
+
if (!isPathInside(host.publicRoot(), abs)) return;
|
|
821
|
+
// A brand-new scene file (not just an edit of an existing one)
|
|
822
|
+
// deserves the same immediate check.
|
|
823
|
+
scheduleFileValidation(abs);
|
|
824
|
+
broadcastPublicAssetChanged(abs);
|
|
825
|
+
|
|
826
|
+
try {
|
|
827
|
+
const { hash, size } = await hashAndSize(abs);
|
|
828
|
+
fileHashCache.set(abs, hash);
|
|
829
|
+
fileSizeCache.set(abs, size);
|
|
830
|
+
void recordFilesystemMutation(abs, hash);
|
|
831
|
+
|
|
832
|
+
// SC2: only treat a delete+create as a move when it is unambiguous —
|
|
833
|
+
// matching hash AND size, and a single such pending unlink. Otherwise
|
|
834
|
+
// an unrelated delete+create could silently rewrite scene refs.
|
|
835
|
+
const match = findUniqueMoveMatch(pendingUnlinks, hash, size, Date.now(), MOVE_WINDOW_MS);
|
|
836
|
+
if (match) {
|
|
837
|
+
const idx = pendingUnlinks.indexOf(match);
|
|
838
|
+
if (idx >= 0) pendingUnlinks.splice(idx, 1);
|
|
839
|
+
const newRelPath = `/${relative(host.publicRoot(), abs)}`;
|
|
840
|
+
await handleAssetMove(match.relativePath, newRelPath);
|
|
841
|
+
}
|
|
842
|
+
} catch {
|
|
843
|
+
/* ignore read errors */
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
// ---- Second watcher: <project>/src (W6a) ----
|
|
848
|
+
//
|
|
849
|
+
// The watcher above is scoped to `host.publicRoot()` (`<project>/public`) only —
|
|
850
|
+
// it never sees `src/tools/*.tool.tsx` or `src/**/*.stories.tsx`, so a
|
|
851
|
+
// NEW file there never showed up without a full editor reload (spec §7
|
|
852
|
+
// W4 field note c). Vite's own HMR (`vgai-script-hmr` in dev.ts, and
|
|
853
|
+
// `tool-loader.ts`'s listener on it) only fires on `change`, and the
|
|
854
|
+
// `VGAI_WATCH_POLL` poll-watcher (dev.ts) forwards its synthetic events
|
|
855
|
+
// into VITE's chokidar watcher only — never into this one. So this
|
|
856
|
+
// second watcher is self-contained here: it works in dev AND
|
|
857
|
+
// packaged/prod (this module is shared by both), and on drvfs
|
|
858
|
+
// (`/mnt/c`, no inotify) via the same `VGAI_WATCH_POLL` env var.
|
|
859
|
+
//
|
|
860
|
+
// Tool/story lists use add/unlink. Registered tool modules also use
|
|
861
|
+
// change because their metadata and implementation are Node-loaded.
|
|
862
|
+
//
|
|
863
|
+
// PD-14: this used to be gated on `existsSync(srcDir)`, evaluated ONCE
|
|
864
|
+
// here — so a project with no `src/` at boot got neither this watcher nor
|
|
865
|
+
// the `validateSourceTree` scan below, for the server's entire lifetime.
|
|
866
|
+
// That is the exact shape of an agent scaffolding into an already-open
|
|
867
|
+
// editor, and of a `src/` deleted and restored (a branch switch, a
|
|
868
|
+
// regenerate). The gate is gone: chokidar watches a not-yet-existing path
|
|
869
|
+
// fine — the same property the manifest watcher below already relies on,
|
|
870
|
+
// and measured for a DIRECTORY on chokidar 5 (an `addDir` for `src`
|
|
871
|
+
// itself, then `add` per file, including a fully-populated directory
|
|
872
|
+
// renamed into place and a delete/recreate cycle). Presence is therefore a
|
|
873
|
+
// RUNTIME transition (`addDir`/`unlinkDir` on a watched dir), never a boot
|
|
874
|
+
// fact.
|
|
875
|
+
if (host.projectRoot() !== engineRoot) {
|
|
876
|
+
// Every STORY SOURCE DIR the manifest implies, not just `<project>/src`
|
|
877
|
+
// — for a project whose root entry lives outside its folder (a
|
|
878
|
+
// repo-vendored game) that game's own source tree is where its stories
|
|
879
|
+
// are, so watching only `<project>/src` would leave "add a story, the
|
|
880
|
+
// board appears" dead there while it works everywhere else. Same set the
|
|
881
|
+
// `/__editor/story-files` scan uses; `projectStorySourceDirs` always
|
|
882
|
+
// leads with `<project>/src`, so the tools half below is unaffected.
|
|
883
|
+
//
|
|
884
|
+
// VALIDATION reads the same set (`sourceValidationDirs`). The two halves
|
|
885
|
+
// used to disagree — this watcher derived, the validation scan and its
|
|
886
|
+
// "is it running" predicate hard-coded to `<project>/src` — so a
|
|
887
|
+
// repo-vendored game watched the right tree and reported, permanently,
|
|
888
|
+
// that nothing was being checked.
|
|
889
|
+
const watchDirs = projectStorySourceDirs(host.projectRoot(), readProjectManifest());
|
|
890
|
+
sourceValidationDirs = watchDirs;
|
|
891
|
+
// #131: same polling decision as the host.publicRoot() watcher above (env
|
|
892
|
+
// contract matches dev.ts's poller; auto-on for drvfs projects).
|
|
893
|
+
srcWatcher = chokidar.watch(watchDirs, {
|
|
894
|
+
ignoreInitial: true,
|
|
895
|
+
ignored: /(^|[/\\])\../, // ignore dotfiles
|
|
896
|
+
...pollOptions,
|
|
897
|
+
});
|
|
898
|
+
const onListChange = (filePath: string) => {
|
|
899
|
+
const rel = relative(host.projectRoot(), resolve(filePath)).split(sep).join('/');
|
|
900
|
+
const kind = classifyProjectSrcPath(rel);
|
|
901
|
+
const event = kind
|
|
902
|
+
? {
|
|
903
|
+
tools: 'tool-files-changed',
|
|
904
|
+
stories: 'story-files-changed',
|
|
905
|
+
}[kind]
|
|
906
|
+
: // Everything else under src/ — a model module, a page, a data
|
|
907
|
+
// file a contributed FINDER reads. The adapter table is built
|
|
908
|
+
// from those files and nothing imports them, so no HMR reaches
|
|
909
|
+
// the page when one appears: a models project's Content panel
|
|
910
|
+
// did not list a new `src/models/*.ts` until something unrelated
|
|
911
|
+
// refreshed the table (measured 2026-09-06, an owner watching an
|
|
912
|
+
// agent's mushroom sit as "a ts file"). The page refreshes the
|
|
913
|
+
// table when the path matches a finder's include glob.
|
|
914
|
+
'source-files-changed';
|
|
915
|
+
broadcastDebounced(event, { path: rel });
|
|
916
|
+
};
|
|
917
|
+
// Journal every event first — the durable answer to "what did the
|
|
918
|
+
// watcher see" when a page reloads or a table refreshes for no visible
|
|
919
|
+
// reason (2026-09-06: three reloads on tsconfig 'changes' whose mtime
|
|
920
|
+
// never moved, and hot-updates for files a scaffold had deleted
|
|
921
|
+
// minutes before the server started).
|
|
922
|
+
srcWatcher.on('all', (event, filePath) => {
|
|
923
|
+
const rel = relative(host.projectRoot(), resolve(filePath)).split(sep).join('/');
|
|
924
|
+
let mtimeMs: number | null = null;
|
|
925
|
+
try {
|
|
926
|
+
mtimeMs = statSync(filePath).mtimeMs;
|
|
927
|
+
} catch {
|
|
928
|
+
/* gone */
|
|
929
|
+
}
|
|
930
|
+
journalEvent({ kind: 'src-watch', event, path: rel, mtimeMs });
|
|
931
|
+
});
|
|
932
|
+
srcWatcher.on('add', onListChange);
|
|
933
|
+
srcWatcher.on('unlink', onListChange);
|
|
934
|
+
// CHANGE too (blind-walk beat 9): a story saved from the editor's own
|
|
935
|
+
// header MODIFIES an existing .stories.tsx — add/unlink alone meant the
|
|
936
|
+
// new export was invisible until a full page reload, and a
|
|
937
|
+
// rename-then-delete chained on the stale list. Hand edits to a
|
|
938
|
+
// stories/tools file get the same refresh, which was its own gap.
|
|
939
|
+
srcWatcher.on('change', onListChange);
|
|
940
|
+
srcWatcher.on('add', invalidateComponentIndex);
|
|
941
|
+
srcWatcher.on('change', invalidateComponentIndex);
|
|
942
|
+
srcWatcher.on('unlink', invalidateComponentIndex);
|
|
943
|
+
// Registered callable tool edits need a fresh Node-side catalog read;
|
|
944
|
+
// contribution modules still ride ordinary Vite HMR in the browser.
|
|
945
|
+
srcWatcher.on('change', (filePath) => {
|
|
946
|
+
const rel = relative(host.projectRoot(), resolve(filePath)).split(sep).join('/');
|
|
947
|
+
if (classifyProjectSrcPath(rel) === 'tools') {
|
|
948
|
+
broadcastDebounced('tool-files-changed', { path: rel });
|
|
949
|
+
}
|
|
950
|
+
});
|
|
951
|
+
srcWatcher.on('add', (filePath) => scheduleFileValidation(resolve(filePath)));
|
|
952
|
+
srcWatcher.on('change', (filePath) => scheduleFileValidation(resolve(filePath)));
|
|
953
|
+
srcWatcher.on('unlink', (filePath) => clearFileValidation(resolve(filePath)));
|
|
954
|
+
srcWatcher.on('add', (filePath) => void recordFilesystemMutation(resolve(filePath)));
|
|
955
|
+
srcWatcher.on('change', (filePath) => void recordFilesystemMutation(resolve(filePath)));
|
|
956
|
+
srcWatcher.on('unlink', (filePath) => void recordFilesystemMutation(resolve(filePath), null));
|
|
957
|
+
|
|
958
|
+
// PD-14: a source dir materializing is the boot that never happened —
|
|
959
|
+
// run the SAME scan boot runs. The per-file `add` events above already
|
|
960
|
+
// cover a directory populated one file at a time, but a tree moved or
|
|
961
|
+
// extracted into place atomically can deliver its contents in one
|
|
962
|
+
// filesystem event, and the scan is a single readdir walk, so it stays
|
|
963
|
+
// the authority on "everything that is there right now" rather than a
|
|
964
|
+
// bet on event granularity.
|
|
965
|
+
//
|
|
966
|
+
// Both handlers ask the FILESYSTEM rather than comparing the event path
|
|
967
|
+
// to a watched root: this watcher is rooted at exactly those dirs, so any
|
|
968
|
+
// `addDir` from it already proves one exists, and `existsSync`
|
|
969
|
+
// distinguishes "a watched root itself went away" from "a subdirectory
|
|
970
|
+
// under it did" without depending on whether chokidar hands paths back
|
|
971
|
+
// symlink-resolved.
|
|
972
|
+
const scanSourceDirs = (): void => {
|
|
973
|
+
for (const dir of sourceValidationDirs) if (existsSync(dir)) void validateSourceTree(dir);
|
|
974
|
+
};
|
|
975
|
+
const anySourceDirExists = (): boolean => sourceValidationDirs.some((dir) => existsSync(dir));
|
|
976
|
+
srcWatcher.on('addDir', () => {
|
|
977
|
+
if (sourceValidation === 'active') return;
|
|
978
|
+
setSourceValidation('active');
|
|
979
|
+
scanSourceDirs();
|
|
980
|
+
});
|
|
981
|
+
srcWatcher.on('unlinkDir', () => {
|
|
982
|
+
if (anySourceDirExists()) return;
|
|
983
|
+
setSourceValidation('awaiting-src');
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
if (anySourceDirExists()) {
|
|
987
|
+
setSourceValidation('active');
|
|
988
|
+
scanSourceDirs();
|
|
989
|
+
} else {
|
|
990
|
+
setSourceValidation('awaiting-src');
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// ---- Third watcher: <project>/references (reference material) ----
|
|
995
|
+
//
|
|
996
|
+
// Reference media — moodboards, generated stills and clips — lives beside
|
|
997
|
+
// `public/` and is listed by Content like any other media, so a file that
|
|
998
|
+
// lands there must appear WITHOUT anyone pressing refresh. That is this
|
|
999
|
+
// watcher's whole job, and it is deliberately the thinnest of the three:
|
|
1000
|
+
// references are not shipped, so there is no hashing, no validation, no
|
|
1001
|
+
// move-rewriting and no collaboration record — nothing downstream of
|
|
1002
|
+
// `public/`'s semantics applies to a file the build never copies.
|
|
1003
|
+
//
|
|
1004
|
+
// It broadcasts the UNSCOPED `assets-changed`, the same shape the asset
|
|
1005
|
+
// library uses: the paths in a scoped event are PUBLIC-relative by
|
|
1006
|
+
// contract, and a `references/…` string in that field would be
|
|
1007
|
+
// indistinguishable from a public folder named `references`.
|
|
1008
|
+
if (host.projectRoot() !== engineRoot) {
|
|
1009
|
+
referencesWatcher = chokidar.watch(join(host.projectRoot(), 'references'), {
|
|
1010
|
+
ignoreInitial: true,
|
|
1011
|
+
ignored: /(^|[/\\])\../,
|
|
1012
|
+
...pollOptions,
|
|
1013
|
+
});
|
|
1014
|
+
const onReferenceChange = (event: string): void => {
|
|
1015
|
+
if (event !== 'add' && event !== 'change' && event !== 'unlink') return;
|
|
1016
|
+
broadcastDebounced('assets-changed', {});
|
|
1017
|
+
};
|
|
1018
|
+
referencesWatcher.on('all', onReferenceChange);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// ---- Fourth watcher: <project>/vgai.project.json (#103 validate-on-change) ----
|
|
1022
|
+
//
|
|
1023
|
+
// The manifest lives at the PROJECT ROOT, outside both `host.publicRoot()` and
|
|
1024
|
+
// `src/` — neither watcher above ever sees it. It's a single fixed-name
|
|
1025
|
+
// file, so this watches that exact path rather than a directory (chokidar
|
|
1026
|
+
// supports watching a not-yet-existing file and reports its later `add`).
|
|
1027
|
+
if (host.projectRoot() !== engineRoot) {
|
|
1028
|
+
const manifestPath = resolveManifestPath(host.projectRoot());
|
|
1029
|
+
// #131: same polling decision as the watchers above. There is ONE
|
|
1030
|
+
// manifest filename, and it can appear later (chokidar watches
|
|
1031
|
+
// not-yet-existing paths fine).
|
|
1032
|
+
const adapterModulePath = join(host.projectRoot(), ADAPTER_MODULE_FILENAME);
|
|
1033
|
+
manifestWatcher = chokidar.watch(
|
|
1034
|
+
[join(host.projectRoot(), MANIFEST_FILENAME), adapterModulePath],
|
|
1035
|
+
{
|
|
1036
|
+
ignoreInitial: true,
|
|
1037
|
+
...pollOptions,
|
|
1038
|
+
},
|
|
1039
|
+
);
|
|
1040
|
+
const onDeclarationChange = (event: string, filePath: string): void => {
|
|
1041
|
+
if (event !== 'add' && event !== 'change' && event !== 'unlink') return;
|
|
1042
|
+
invalidateComponentIndex();
|
|
1043
|
+
broadcastDebounced('project-declarations-changed', {});
|
|
1044
|
+
const absPath = resolve(filePath);
|
|
1045
|
+
if (absPath !== manifestPath) return;
|
|
1046
|
+
if (event === 'unlink') {
|
|
1047
|
+
clearFileValidation(manifestPath);
|
|
1048
|
+
void recordFilesystemMutation(manifestPath, null);
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
scheduleFileValidation(manifestPath);
|
|
1052
|
+
void recordFilesystemMutation(manifestPath);
|
|
1053
|
+
};
|
|
1054
|
+
manifestWatcher.on('all', onDeclarationChange);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// Warm the transient index behind ordinary editor boot. The request path
|
|
1058
|
+
// still awaits this same promise, and failures are retried there rather
|
|
1059
|
+
// than becoming an unhandled background rejection.
|
|
1060
|
+
void currentProjectComponents().catch(() => {
|
|
1061
|
+
componentIndexCache = null;
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// Start initial watcher
|
|
1066
|
+
startWatcher();
|
|
1067
|
+
|
|
1068
|
+
// Periodic cleanup of stale pending unlinks
|
|
1069
|
+
const moveSweepTimer = setInterval(() => {
|
|
1070
|
+
const cutoff = Date.now() - MOVE_WINDOW_MS * 2;
|
|
1071
|
+
while (pendingUnlinks.length > 0 && pendingUnlinks[0]!.timestamp < cutoff) {
|
|
1072
|
+
pendingUnlinks.shift();
|
|
1073
|
+
}
|
|
1074
|
+
}, 5000);
|
|
1075
|
+
return {
|
|
1076
|
+
start: startWatcher,
|
|
1077
|
+
currentProjectComponents,
|
|
1078
|
+
currentValidationLogEntries,
|
|
1079
|
+
announceBuildDisciplineTripwires,
|
|
1080
|
+
projectValidation,
|
|
1081
|
+
projectWarnings,
|
|
1082
|
+
expectedEditorMutations,
|
|
1083
|
+
get sourceValidation() {
|
|
1084
|
+
return sourceValidation;
|
|
1085
|
+
},
|
|
1086
|
+
set sourceValidation(next: SourceValidationState) {
|
|
1087
|
+
sourceValidation = next;
|
|
1088
|
+
},
|
|
1089
|
+
get cadenceGate() {
|
|
1090
|
+
return cadenceGate;
|
|
1091
|
+
},
|
|
1092
|
+
set cadenceGate(next: TripwireGate) {
|
|
1093
|
+
cadenceGate = next;
|
|
1094
|
+
},
|
|
1095
|
+
get unplayedGate() {
|
|
1096
|
+
return unplayedGate;
|
|
1097
|
+
},
|
|
1098
|
+
set unplayedGate(next: TripwireGate) {
|
|
1099
|
+
unplayedGate = next;
|
|
1100
|
+
},
|
|
1101
|
+
get publicAssetsLastChangedAt() {
|
|
1102
|
+
return publicAssetsLastChangedAt;
|
|
1103
|
+
},
|
|
1104
|
+
set publicAssetsLastChangedAt(next: number | null) {
|
|
1105
|
+
publicAssetsLastChangedAt = next;
|
|
1106
|
+
},
|
|
1107
|
+
get publicAssetsLastPath() {
|
|
1108
|
+
return publicAssetsLastPath;
|
|
1109
|
+
},
|
|
1110
|
+
set publicAssetsLastPath(next: string | null) {
|
|
1111
|
+
publicAssetsLastPath = next;
|
|
1112
|
+
},
|
|
1113
|
+
get publicAssetsChangedCount() {
|
|
1114
|
+
return publicAssetsChangedCount;
|
|
1115
|
+
},
|
|
1116
|
+
set publicAssetsChangedCount(next: number) {
|
|
1117
|
+
publicAssetsChangedCount = next;
|
|
1118
|
+
},
|
|
1119
|
+
close: async () => {
|
|
1120
|
+
clearInterval(moveSweepTimer);
|
|
1121
|
+
for (const timer of validationDebounceTimers.values()) clearTimeout(timer);
|
|
1122
|
+
validationDebounceTimers.clear();
|
|
1123
|
+
for (const timer of pendingBroadcasts.values()) clearTimeout(timer);
|
|
1124
|
+
pendingBroadcasts.clear();
|
|
1125
|
+
if (componentIndexWarmTimer) clearTimeout(componentIndexWarmTimer);
|
|
1126
|
+
await Promise.all([
|
|
1127
|
+
watcher?.close(),
|
|
1128
|
+
srcWatcher?.close(),
|
|
1129
|
+
referencesWatcher?.close(),
|
|
1130
|
+
manifestWatcher?.close(),
|
|
1131
|
+
]);
|
|
1132
|
+
},
|
|
1133
|
+
};
|
|
1134
|
+
}
|