@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.
Files changed (719) hide show
  1. package/BUNDLED_NOTICES +1527 -0
  2. package/LICENSE +686 -0
  3. package/LICENSE-APACHE +202 -0
  4. package/NOTICE +18 -0
  5. package/README.md +24 -0
  6. package/dist/build/chunk-JS4D4XZ3.js +175 -0
  7. package/dist/build/chunk-JS4D4XZ3.js.map +7 -0
  8. package/dist/build/vite-plugin-product-contributions.js +100 -0
  9. package/dist/build/vite-plugin-product-contributions.js.map +7 -0
  10. package/dist/build/vite-plugin-shared-react.js +25 -0
  11. package/dist/build/vite-plugin-shared-react.js.map +7 -0
  12. package/dist/build/vite-plugin-shared-three.js +86 -0
  13. package/dist/build/vite-plugin-shared-three.js.map +7 -0
  14. package/dist/server/chunk-KMJC7AGO.js +98 -0
  15. package/dist/server/chunk-KMJC7AGO.js.map +7 -0
  16. package/dist/server/chunk-PSILUMGS.js +46 -0
  17. package/dist/server/chunk-PSILUMGS.js.map +7 -0
  18. package/dist/server/frame-proxy.js +255 -0
  19. package/dist/server/frame-proxy.js.map +7 -0
  20. package/dist/server/open-browser.js +53 -0
  21. package/dist/server/open-browser.js.map +7 -0
  22. package/dist/server/process-shutdown.js +127 -0
  23. package/dist/server/process-shutdown.js.map +7 -0
  24. package/dist/server/session-registry.js +141 -0
  25. package/dist/server/session-registry.js.map +7 -0
  26. package/dist/server/spawn-opener.js +11 -0
  27. package/dist/server/spawn-opener.js.map +7 -0
  28. package/dist/server/worktree-identity.js +11 -0
  29. package/dist/server/worktree-identity.js.map +7 -0
  30. package/dist-server/packaged.mjs +88816 -0
  31. package/package.json +118 -0
  32. package/scripts/build-plugins.mjs +14 -0
  33. package/scripts/build-server.mjs +26 -0
  34. package/scripts/build-session.mjs +9 -0
  35. package/server/account-credentials.ts +60 -0
  36. package/server/account-service.ts +1434 -0
  37. package/server/adapter-region-includes.ts +47 -0
  38. package/server/asset-catalog-v2.ts +175 -0
  39. package/server/asset-history-snapshots.ts +196 -0
  40. package/server/asset-ledger-store.ts +195 -0
  41. package/server/asset-library-routes.ts +1595 -0
  42. package/server/blender-wasm-artifact.ts +430 -0
  43. package/server/boot-timings.ts +106 -0
  44. package/server/canonical-path.ts +50 -0
  45. package/server/checkout-workspace-preflight.ts +349 -0
  46. package/server/cloud-asset-catalog.ts +158 -0
  47. package/server/coding-inference-launch.ts +237 -0
  48. package/server/collaboration-account-client.ts +202 -0
  49. package/server/collaboration-attribution.ts +122 -0
  50. package/server/collaboration-session.ts +934 -0
  51. package/server/comfyui-bridge.ts +248 -0
  52. package/server/console-ledger.ts +499 -0
  53. package/server/creation-site-transform.ts +304 -0
  54. package/server/creation-site-write.ts +648 -0
  55. package/server/data-file-serialize.ts +158 -0
  56. package/server/editor-brand-html.ts +72 -0
  57. package/server/editor-control-socket.ts +366 -0
  58. package/server/editor-server-options.ts +190 -0
  59. package/server/editor-server.ts +1483 -0
  60. package/server/editor-sse.ts +352 -0
  61. package/server/engine-provenance.ts +169 -0
  62. package/server/engine-source-restart.ts +117 -0
  63. package/server/frame-bridge.ts +115 -0
  64. package/server/frame-proxy.ts +490 -0
  65. package/server/frame-workbench.ts +259 -0
  66. package/server/frontend-handoff.ts +229 -0
  67. package/server/game-globals-shadow.ts +113 -0
  68. package/server/gameplay-session-retention.ts +149 -0
  69. package/server/gameplay-sessions.ts +123 -0
  70. package/server/generation-jobs.ts +201 -0
  71. package/server/generation-reconciler.ts +137 -0
  72. package/server/generative-execution-context.ts +59 -0
  73. package/server/git-workflow.ts +612 -0
  74. package/server/harness-chat-caller.ts +36 -0
  75. package/server/harness-chat-service.ts +1681 -0
  76. package/server/idle-shutdown.ts +170 -0
  77. package/server/js-profiling-policy.ts +52 -0
  78. package/server/launcher-settings.ts +84 -0
  79. package/server/local-asset-catalog.ts +410 -0
  80. package/server/managed-account-defaults.ts +22 -0
  81. package/server/mock-control-plane.ts +540 -0
  82. package/server/model-import-conversion.ts +345 -0
  83. package/server/native-credential-store.ts +162 -0
  84. package/server/open-browser.ts +118 -0
  85. package/server/packaged.ts +1192 -0
  86. package/server/play-stall.ts +126 -0
  87. package/server/process-shutdown.ts +218 -0
  88. package/server/product-presets.ts +55 -0
  89. package/server/project-build-artifact.ts +95 -0
  90. package/server/project-components.ts +100 -0
  91. package/server/project-dependency-invalidation.ts +114 -0
  92. package/server/project-file-scan.ts +235 -0
  93. package/server/project-hmr-files.ts +215 -0
  94. package/server/project-install-roots.ts +210 -0
  95. package/server/project-kinds.ts +71 -0
  96. package/server/project-mcp-servers.ts +82 -0
  97. package/server/project-module-freshness.ts +109 -0
  98. package/server/project-module-instance.ts +375 -0
  99. package/server/project-optimize-deps-entries.ts +827 -0
  100. package/server/project-output-writer.ts +679 -0
  101. package/server/project-package-origin.ts +63 -0
  102. package/server/project-root-surface.ts +831 -0
  103. package/server/project-scratch-path.ts +47 -0
  104. package/server/project-script-hmr.ts +619 -0
  105. package/server/project-serving-plugins.ts +236 -0
  106. package/server/project-tools.ts +653 -0
  107. package/server/project-validation.ts +250 -0
  108. package/server/project-verbs.ts +127 -0
  109. package/server/project-view.ts +52 -0
  110. package/server/project-watch.ts +1134 -0
  111. package/server/project-work-coordinator.ts +288 -0
  112. package/server/provider-credentials.ts +331 -0
  113. package/server/recent-projects-store.ts +98 -0
  114. package/server/redact-secrets.ts +71 -0
  115. package/server/repository-presence.ts +267 -0
  116. package/server/routes/account.ts +357 -0
  117. package/server/routes/agents.ts +60 -0
  118. package/server/routes/assets.ts +154 -0
  119. package/server/routes/build.ts +13 -0
  120. package/server/routes/collaboration.ts +595 -0
  121. package/server/routes/configurations.ts +419 -0
  122. package/server/routes/context.ts +264 -0
  123. package/server/routes/control-plane.ts +1811 -0
  124. package/server/routes/logs.ts +272 -0
  125. package/server/routes/project-identity.ts +302 -0
  126. package/server/routes/project-open.ts +429 -0
  127. package/server/routes/project-source.ts +686 -0
  128. package/server/routes/project-state.ts +383 -0
  129. package/server/routes/relay.ts +1029 -0
  130. package/server/routes/route-helpers.ts +29 -0
  131. package/server/routes/served-modules.ts +138 -0
  132. package/server/routes/session-tabs.ts +196 -0
  133. package/server/routes/settings.ts +121 -0
  134. package/server/routes/share-control.ts +486 -0
  135. package/server/routes/themes.ts +116 -0
  136. package/server/routes/tools.ts +230 -0
  137. package/server/routes/worktrees.ts +595 -0
  138. package/server/scoped-game-css.ts +461 -0
  139. package/server/server-utils.ts +1194 -0
  140. package/server/session-product.ts +112 -0
  141. package/server/session-registry.ts +230 -0
  142. package/server/share-claims.ts +138 -0
  143. package/server/share-host.ts +526 -0
  144. package/server/share-session-gateway.ts +1362 -0
  145. package/server/share-tunnel.ts +123 -0
  146. package/server/spawn-opener.ts +74 -0
  147. package/server/support/play/log-format.ts +76 -0
  148. package/server/support/play/session-record.ts +199 -0
  149. package/server/support/project/build-discipline.ts +718 -0
  150. package/server/support/project/inspection-node.ts +119 -0
  151. package/server/support/project/log-naming.ts +54 -0
  152. package/server/support/project/provenance.ts +290 -0
  153. package/server/support/project/run-name.ts +18 -0
  154. package/server/support/project/session-journal.ts +830 -0
  155. package/server/support/project/shared.ts +327 -0
  156. package/server/tab-bootstrap.ts +62 -0
  157. package/server/tab-heartbeat.ts +477 -0
  158. package/server/tab-lifecycle.ts +550 -0
  159. package/server/tab-presence.ts +1508 -0
  160. package/server/team-agent-mirror.ts +95 -0
  161. package/server/twin-auth.ts +138 -0
  162. package/server/vendored-lock-recorder.ts +968 -0
  163. package/server/worktree-identity.ts +121 -0
  164. package/server/worktree-management.ts +398 -0
  165. package/server/worktree-retention.d.mts +13 -0
  166. package/server/worktree-retention.mjs +205 -0
  167. package/src/EditorContext.tsx +169 -0
  168. package/src/account.ts +333 -0
  169. package/src/action-registry.ts +532 -0
  170. package/src/active-product.ts +74 -0
  171. package/src/active-project.ts +155 -0
  172. package/src/adapter-editor-config.ts +25 -0
  173. package/src/adapter-observation.ts +49 -0
  174. package/src/animation/stage-transport.ts +379 -0
  175. package/src/animation/three-clips-subject.ts +172 -0
  176. package/src/api/asset-library-wire.ts +45 -0
  177. package/src/api/assets.ts +365 -0
  178. package/src/api/base.ts +10 -0
  179. package/src/api/build.ts +99 -0
  180. package/src/api/git-wire.ts +56 -0
  181. package/src/api/logs.ts +92 -0
  182. package/src/api/project-identity.ts +74 -0
  183. package/src/api/project-open.ts +355 -0
  184. package/src/api/project-source.ts +162 -0
  185. package/src/api/project-state.ts +107 -0
  186. package/src/api/relay.ts +268 -0
  187. package/src/api/settings.ts +36 -0
  188. package/src/api/themes.ts +45 -0
  189. package/src/api/worktrees.ts +257 -0
  190. package/src/asset-compare-core.ts +171 -0
  191. package/src/asset-compare.ts +294 -0
  192. package/src/asset-editor-context.tsx +100 -0
  193. package/src/asset-events.ts +97 -0
  194. package/src/asset-inspector-actions.ts +87 -0
  195. package/src/asset-preview-framing.ts +357 -0
  196. package/src/asset-preview.ts +2802 -0
  197. package/src/asset-selection-viewer-registry.ts +113 -0
  198. package/src/asset-selection.ts +133 -0
  199. package/src/asset-workflow/asset-capabilities.ts +344 -0
  200. package/src/asset-workflow/asset-import-jobs.ts +106 -0
  201. package/src/asset-workflow/asset-ledger-backend.ts +126 -0
  202. package/src/asset-workflow/asset-ledger.ts +156 -0
  203. package/src/asset-workflow/asset-materialization-report.ts +140 -0
  204. package/src/asset-workflow/asset-pack-manifest.ts +320 -0
  205. package/src/asset-workflow/asset-types.ts +142 -0
  206. package/src/asset-workflow/asset-workflow-quality.ts +61 -0
  207. package/src/asset-workflow/audio-preview-player.ts +193 -0
  208. package/src/asset-workflow/audio-waveform.ts +22 -0
  209. package/src/asset-workflow/cloud-asset-client.ts +263 -0
  210. package/src/asset-workflow/folder-preview.ts +345 -0
  211. package/src/asset-workflow/hosted-asset-materialization.ts +236 -0
  212. package/src/asset-workflow/image-view-scale.ts +32 -0
  213. package/src/asset-workflow/import-contract.ts +124 -0
  214. package/src/asset-workflow/ledger-write-lock.ts +244 -0
  215. package/src/asset-workflow/model-inspection.ts +818 -0
  216. package/src/asset-workflow/pixi-spritesheet.ts +197 -0
  217. package/src/asset-workflow/preview-resource-lifetime.ts +44 -0
  218. package/src/asset-workflow/project-asset-commands.ts +23 -0
  219. package/src/asset-workflow/project-asset-health.ts +139 -0
  220. package/src/asset-workflow/project-asset-operations.ts +154 -0
  221. package/src/asset-workflow/project-asset-roots.ts +68 -0
  222. package/src/asset-workflow/project-content.ts +288 -0
  223. package/src/asset-workflow/project-source-index.ts +550 -0
  224. package/src/asset-workflow/thumbnail-system.ts +256 -0
  225. package/src/authoring/active-adapter.ts +199 -0
  226. package/src/authoring/active-systems.ts +422 -0
  227. package/src/authoring/adapter-key.ts +18 -0
  228. package/src/authoring/authoring-asset-url.ts +27 -0
  229. package/src/authoring/bootstrap-state.ts +49 -0
  230. package/src/authoring/boundary-authoring-adapter.ts +184 -0
  231. package/src/authoring/breakpoint-state.ts +43 -0
  232. package/src/authoring/canvas-scene-guides.ts +76 -0
  233. package/src/authoring/component-instance-root.ts +171 -0
  234. package/src/authoring/composite-authoring-adapter.ts +2110 -0
  235. package/src/authoring/consumer-actions.ts +520 -0
  236. package/src/authoring/css-numeric-style.ts +97 -0
  237. package/src/authoring/design-time-layers.ts +849 -0
  238. package/src/authoring/design-time-mount-registry.ts +235 -0
  239. package/src/authoring/design-time-settle.ts +343 -0
  240. package/src/authoring/edit-mode-authoring.ts +619 -0
  241. package/src/authoring/eyedropper-session.ts +60 -0
  242. package/src/authoring/instance-source-menu-register.ts +13 -0
  243. package/src/authoring/instance-source-menu.ts +135 -0
  244. package/src/authoring/layered-pick.ts +183 -0
  245. package/src/authoring/live-gesture-lock.ts +50 -0
  246. package/src/authoring/live-object-transform.ts +62 -0
  247. package/src/authoring/mount-failure-report.ts +154 -0
  248. package/src/authoring/mounted-root-subjects.ts +144 -0
  249. package/src/authoring/no-authoring-adapter.ts +55 -0
  250. package/src/authoring/null-inspection-subjects.tsx +75 -0
  251. package/src/authoring/object3d-document-persistence.ts +106 -0
  252. package/src/authoring/object3d-document-session-registry.ts +120 -0
  253. package/src/authoring/object3d-document-session.ts +1326 -0
  254. package/src/authoring/object3d-gesture-controller.ts +113 -0
  255. package/src/authoring/panel-authoring.ts +121 -0
  256. package/src/authoring/prefab-instance-inspector-section.tsx +237 -0
  257. package/src/authoring/prefab-instance-section-model.ts +55 -0
  258. package/src/authoring/project-authoring-session.ts +105 -0
  259. package/src/authoring/provenance.ts +99 -0
  260. package/src/authoring/quarks-particle-systems.ts +19 -0
  261. package/src/authoring/react-canvas-navigation.ts +255 -0
  262. package/src/authoring/react-design-canvas-style.ts +20 -0
  263. package/src/authoring/react-story-board.ts +937 -0
  264. package/src/authoring/selection-scope.ts +195 -0
  265. package/src/authoring/shell-document-ops.ts +169 -0
  266. package/src/authoring/shell-object3d-document-write-policy.ts +108 -0
  267. package/src/authoring/shell-viewport-policy.ts +48 -0
  268. package/src/authoring/source-object3d-authoring-adapter.ts +526 -0
  269. package/src/authoring/stories-scope.ts +35 -0
  270. package/src/authoring/story-board-chrome-fit.ts +107 -0
  271. package/src/authoring/story-board-presentation.ts +111 -0
  272. package/src/authoring/three-projection-core.ts +226 -0
  273. package/src/authoring/viewport-pick-context.ts +40 -0
  274. package/src/authoring/viewport-raycast.ts +240 -0
  275. package/src/authoring/viewport-tool-context.ts +73 -0
  276. package/src/authoring/world-canvas-viewport-state.ts +35 -0
  277. package/src/authoring/world-hidden-viewport.ts +152 -0
  278. package/src/authoring/world-pan-state.ts +198 -0
  279. package/src/authoring/world-session-state.ts +101 -0
  280. package/src/authoring/write-pipe.ts +173 -0
  281. package/src/availability-tick.ts +66 -0
  282. package/src/bitmap-label.ts +120 -0
  283. package/src/blender-tab-metrics.ts +161 -0
  284. package/src/board-open-actions.ts +20 -0
  285. package/src/boot-routing.ts +391 -0
  286. package/src/build-identity.ts +15 -0
  287. package/src/bytes-codec.ts +62 -0
  288. package/src/camera-authoring.ts +175 -0
  289. package/src/cancellation-reason.ts +58 -0
  290. package/src/canvas-preview-frames.ts +482 -0
  291. package/src/capture-camera-pose.ts +77 -0
  292. package/src/chrome-registry.ts +159 -0
  293. package/src/chrome-slot-registry.ts +91 -0
  294. package/src/collaboration-client.ts +264 -0
  295. package/src/collaboration-presence.ts +41 -0
  296. package/src/command-dispatch.ts +19 -0
  297. package/src/command-listener.ts +2743 -0
  298. package/src/command-registry.ts +70 -0
  299. package/src/component-board-registry.ts +205 -0
  300. package/src/component-states-registry.ts +200 -0
  301. package/src/components/AgentPresentationNotice.tsx +75 -0
  302. package/src/components/AlignToolbar.tsx +204 -0
  303. package/src/components/AppRoot.tsx +394 -0
  304. package/src/components/ApplicationMenus.tsx +415 -0
  305. package/src/components/AssetBrowser.tsx +2843 -0
  306. package/src/components/AssetEditorShell.tsx +216 -0
  307. package/src/components/AssetInspectorToolSection.tsx +124 -0
  308. package/src/components/BoardRulers.tsx +354 -0
  309. package/src/components/CameraInfo.tsx +81 -0
  310. package/src/components/CanvasSceneViewport.tsx +556 -0
  311. package/src/components/CapabilityCoverageSection.tsx +185 -0
  312. package/src/components/CenterDocuments.tsx +22 -0
  313. package/src/components/ChromeSlot.tsx +20 -0
  314. package/src/components/CodeView.tsx +470 -0
  315. package/src/components/CollaborationHeaderControl.css +74 -0
  316. package/src/components/CompactInspectorCard.tsx +190 -0
  317. package/src/components/CompactInspectorShell.tsx +39 -0
  318. package/src/components/ConsolePanel.css +55 -0
  319. package/src/components/ConsolePanel.tsx +270 -0
  320. package/src/components/DefaultEditorLayout.tsx +106 -0
  321. package/src/components/DocumentHeaderStrip.tsx +96 -0
  322. package/src/components/DocumentShelfRail.tsx +45 -0
  323. package/src/components/DocumentThumbnail.tsx +345 -0
  324. package/src/components/EditorLeaseGuard.tsx +372 -0
  325. package/src/components/ErrorBanner.tsx +123 -0
  326. package/src/components/FolderPreviewTile.tsx +159 -0
  327. package/src/components/GameHierarchy.tsx +3842 -0
  328. package/src/components/GameplaySessionTimeline.tsx +295 -0
  329. package/src/components/InspectionProjection.tsx +968 -0
  330. package/src/components/Inspector.tsx +233 -0
  331. package/src/components/InspectorCanvasPreview.tsx +35 -0
  332. package/src/components/InspectorFieldsSection.tsx +290 -0
  333. package/src/components/InspectorObjectPreview.tsx +57 -0
  334. package/src/components/InspectorStoriesSection.tsx +92 -0
  335. package/src/components/InspectorToolSection.tsx +96 -0
  336. package/src/components/InspectorTransformSection.tsx +245 -0
  337. package/src/components/LightExplorerPanel.tsx +433 -0
  338. package/src/components/MediaProperties.tsx +145 -0
  339. package/src/components/NonThreeAuthoringBootstrap.tsx +19 -0
  340. package/src/components/Object3DDocumentToolbar.css +100 -0
  341. package/src/components/Object3DDocumentToolbar.tsx +454 -0
  342. package/src/components/Object3DDocumentViewport.tsx +58 -0
  343. package/src/components/ProjectHeader.css +617 -0
  344. package/src/components/ProjectHeader.tsx +328 -0
  345. package/src/components/ProjectLayout.tsx +249 -0
  346. package/src/components/ReactCanvasControls.tsx +284 -0
  347. package/src/components/RootSelectionOverlay.tsx +3162 -0
  348. package/src/components/RootTextEditor.tsx +79 -0
  349. package/src/components/SaveStatus.tsx +70 -0
  350. package/src/components/StageHost.tsx +1954 -0
  351. package/src/components/StageOverlays.tsx +21 -0
  352. package/src/components/StartupErrorScreen.tsx +76 -0
  353. package/src/components/StartupLoadingScreen.tsx +65 -0
  354. package/src/components/StatsOverlay.tsx +78 -0
  355. package/src/components/SurfaceStateOverlay.tsx +24 -0
  356. package/src/components/ToolContributionSurfaces.tsx +65 -0
  357. package/src/components/ToolHost.tsx +370 -0
  358. package/src/components/ToolObject3DPreview.tsx +39 -0
  359. package/src/components/Toolbar.tsx +595 -0
  360. package/src/components/TransientHint.tsx +44 -0
  361. package/src/components/TransportStrip.tsx +174 -0
  362. package/src/components/VersionControlSection.tsx +470 -0
  363. package/src/components/VgaiLogo.css +83 -0
  364. package/src/components/VgaiLogo.tsx +35 -0
  365. package/src/components/ViewportControlsHint.tsx +60 -0
  366. package/src/components/ViewportFurniture.tsx +401 -0
  367. package/src/components/ViewportOverlay.tsx +145 -0
  368. package/src/components/ViewportOverlaysMenu.tsx +153 -0
  369. package/src/components/ViewportShadingMenu.tsx +300 -0
  370. package/src/components/ViewportViewMenu.tsx +101 -0
  371. package/src/components/WorkspaceDocumentSurface.tsx +204 -0
  372. package/src/components/WorkspaceUtilitySurface.tsx +22 -0
  373. package/src/components/WorktreeSwitcher.css +239 -0
  374. package/src/components/WorktreeSwitcher.tsx +890 -0
  375. package/src/components/account-documents.tsx +1162 -0
  376. package/src/components/asset-documents.tsx +812 -0
  377. package/src/components/asset-editor-persistence.ts +216 -0
  378. package/src/components/asset-selection-section.tsx +535 -0
  379. package/src/components/asset-thumbnails.tsx +312 -0
  380. package/src/components/asset-viewers/AudioViewer.tsx +201 -0
  381. package/src/components/asset-viewers/EntityModelDocument.tsx +122 -0
  382. package/src/components/asset-viewers/EnvironmentAssetDocument.tsx +440 -0
  383. package/src/components/asset-viewers/GenericJsonViewer.tsx +102 -0
  384. package/src/components/asset-viewers/ImageViewer.tsx +300 -0
  385. package/src/components/asset-viewers/JsonAssetDocument.tsx +93 -0
  386. package/src/components/asset-viewers/LiveModuleDocument.tsx +417 -0
  387. package/src/components/asset-viewers/LutAssetDocument.tsx +444 -0
  388. package/src/components/asset-viewers/ModelAssetDocument.tsx +105 -0
  389. package/src/components/asset-viewers/Object3DPreview.tsx +359 -0
  390. package/src/components/asset-viewers/OnlineAssetDetail.tsx +421 -0
  391. package/src/components/asset-viewers/PasteboardModuleDocument.tsx +67 -0
  392. package/src/components/asset-viewers/QuarksAssetDocument.tsx +526 -0
  393. package/src/components/asset-viewers/ShaderAssetDocument.tsx +743 -0
  394. package/src/components/asset-viewers/SourceAssetViewer.tsx +281 -0
  395. package/src/components/asset-viewers/SpritesheetSpriteView.tsx +102 -0
  396. package/src/components/asset-viewers/VideoViewer.tsx +101 -0
  397. package/src/components/asset-viewers/shader-source.ts +144 -0
  398. package/src/components/asset-workflow.css +731 -0
  399. package/src/components/board-guides.ts +150 -0
  400. package/src/components/compact-inspector.css +539 -0
  401. package/src/components/core-utilities.tsx +90 -0
  402. package/src/components/editor-notifications.css +40 -0
  403. package/src/components/engine-workspace.css +274 -0
  404. package/src/components/inspector-preview-section.tsx +238 -0
  405. package/src/components/inspector-property-grouping.ts +64 -0
  406. package/src/components/inspector-revert-label.ts +20 -0
  407. package/src/components/inspector-selection.ts +42 -0
  408. package/src/components/inspector-stories-gating.ts +171 -0
  409. package/src/components/inspector-transform-subject.ts +11 -0
  410. package/src/components/inspector-transform.ts +75 -0
  411. package/src/components/kind-documents.tsx +500 -0
  412. package/src/components/palette-action-publisher.tsx +140 -0
  413. package/src/components/primitives/DraftColorInput.tsx +74 -0
  414. package/src/components/product-shell.css +1829 -0
  415. package/src/components/project-tool-documents.tsx +410 -0
  416. package/src/components/scene-documents.tsx +223 -0
  417. package/src/components/stage-keyboard.tsx +37 -0
  418. package/src/components/stage-overlay-set.tsx +107 -0
  419. package/src/components/stage-presence-markers.ts +478 -0
  420. package/src/components/standard-viewport-dressing.ts +417 -0
  421. package/src/components/status-contributions.tsx +403 -0
  422. package/src/components/tool-documents.tsx +309 -0
  423. package/src/components/tool-schema-form.tsx +262 -0
  424. package/src/components/use-after-paint.ts +41 -0
  425. package/src/components/use-project-image-assets.ts +86 -0
  426. package/src/components/viewport-header-controls.css +90 -0
  427. package/src/components/viewport-surface-status.tsx +55 -0
  428. package/src/components/workspace-history.ts +32 -0
  429. package/src/components/workspace-static-panel-registry.tsx +152 -0
  430. package/src/components/workspace-surfaces.css +621 -0
  431. package/src/components/world-documents.tsx +586 -0
  432. package/src/components/world-overlay-gestures.ts +1694 -0
  433. package/src/composite-screenshot.ts +1641 -0
  434. package/src/console-sync.ts +131 -0
  435. package/src/constraint-helper.ts +338 -0
  436. package/src/content-entry-source-registry.ts +184 -0
  437. package/src/coverage/authoring-seam-evidence.ts +300 -0
  438. package/src/coverage/canvas-reveal.ts +192 -0
  439. package/src/coverage/design-time-surfaces.ts +101 -0
  440. package/src/coverage/live-seam-evidence.ts +11 -0
  441. package/src/coverage/ontology-invariants.ts +466 -0
  442. package/src/coverage/session-vitals.ts +501 -0
  443. package/src/coverage/system-seam-evidence.ts +72 -0
  444. package/src/crash-null-boundary.ts +36 -0
  445. package/src/creation-site-edit.ts +1479 -0
  446. package/src/creation-site-registry.ts +160 -0
  447. package/src/delegate-harness-registry.ts +143 -0
  448. package/src/document-context-registry.ts +177 -0
  449. package/src/document-open-registry.ts +200 -0
  450. package/src/document-preview-source.ts +20 -0
  451. package/src/document-renderer-session.ts +138 -0
  452. package/src/editor-api.ts +46 -0
  453. package/src/editor-chrome-capture.ts +139 -0
  454. package/src/editor-commands.ts +164 -0
  455. package/src/editor-console.ts +563 -0
  456. package/src/editor-current-view.ts +82 -0
  457. package/src/editor-document-probe.ts +881 -0
  458. package/src/editor-git-client.ts +115 -0
  459. package/src/editor-host-door.ts +462 -0
  460. package/src/editor-hotkeys.ts +842 -0
  461. package/src/editor-lease-view.ts +39 -0
  462. package/src/editor-lease.ts +415 -0
  463. package/src/editor-mode.ts +19 -0
  464. package/src/editor-notifications.ts +140 -0
  465. package/src/editor-presence.ts +563 -0
  466. package/src/editor-presentation-activity.ts +50 -0
  467. package/src/editor-presentation-notice.ts +42 -0
  468. package/src/editor-runtime.tsx +145 -0
  469. package/src/editor-server-response.ts +86 -0
  470. package/src/editor-session-attribution.ts +75 -0
  471. package/src/editor-session-mode.ts +54 -0
  472. package/src/editor-shell-store.ts +1304 -0
  473. package/src/editor-state-facets.ts +74 -0
  474. package/src/editor-styles.css +33 -0
  475. package/src/editor-view-presentation.ts +810 -0
  476. package/src/editor-viewport.ts +5302 -0
  477. package/src/entity-lod.ts +31 -0
  478. package/src/entity-object.ts +91 -0
  479. package/src/files/file-provider.ts +62 -0
  480. package/src/files/project-files.ts +264 -0
  481. package/src/finders/index.ts +136 -0
  482. package/src/finders/scenes-from-entrypoint-selection.ts +387 -0
  483. package/src/frame/bridge.tsx +1408 -0
  484. package/src/frame/product.ts +77 -0
  485. package/src/gameplay-dom-recording.ts +319 -0
  486. package/src/gameplay-export-state.ts +14 -0
  487. package/src/gameplay-replay.ts +417 -0
  488. package/src/gameplay-session-time.ts +9 -0
  489. package/src/gameplay-sessions.ts +204 -0
  490. package/src/harness-chat-types.ts +253 -0
  491. package/src/hierarchy-component-marks.ts +298 -0
  492. package/src/hierarchy-drop.ts +91 -0
  493. package/src/hierarchy-expansion-state.ts +80 -0
  494. package/src/hierarchy-header-slot.ts +52 -0
  495. package/src/hierarchy-internals.ts +197 -0
  496. package/src/hierarchy-kind-icon.ts +217 -0
  497. package/src/hierarchy-mark-reader.ts +73 -0
  498. package/src/hierarchy-menu-registry.ts +67 -0
  499. package/src/hierarchy-node-rows.ts +307 -0
  500. package/src/hierarchy-panel-view.ts +280 -0
  501. package/src/hierarchy-projection.ts +76 -0
  502. package/src/hierarchy-row-cache.ts +243 -0
  503. package/src/hierarchy-row-model.ts +308 -0
  504. package/src/hierarchy-rows.ts +11 -0
  505. package/src/hierarchy-walk.ts +86 -0
  506. package/src/history/editor-session.ts +25 -0
  507. package/src/history/history-commands.ts +147 -0
  508. package/src/history/history-delegate.ts +163 -0
  509. package/src/history/history-limit-notices.ts +43 -0
  510. package/src/history/history-service.ts +1173 -0
  511. package/src/history/persistence-coordinator.ts +35 -0
  512. package/src/history/project-file-history.ts +386 -0
  513. package/src/history/project-root-history-backends.ts +139 -0
  514. package/src/history/resource-registry.ts +209 -0
  515. package/src/history/snapshot-store.ts +103 -0
  516. package/src/history/source-history-backend.ts +546 -0
  517. package/src/history/types.ts +124 -0
  518. package/src/hmr-registration-group.ts +67 -0
  519. package/src/hmr-stable-react-context.ts +23 -0
  520. package/src/hotkeys.ts +188 -0
  521. package/src/inference-diagnostics.ts +69 -0
  522. package/src/initial-project.ts +80 -0
  523. package/src/inspection/active-subject.ts +578 -0
  524. package/src/inspection/active-surface.ts +142 -0
  525. package/src/inspection/compose.ts +1064 -0
  526. package/src/inspection/display.ts +170 -0
  527. package/src/inspection/document-subject.ts +109 -0
  528. package/src/inspection/game-subject.ts +85 -0
  529. package/src/inspection/model.ts +542 -0
  530. package/src/inspection/null-subject.ts +115 -0
  531. package/src/inspection/serialize.ts +357 -0
  532. package/src/inspection/use-active-inspection.ts +180 -0
  533. package/src/inspector-presentation.ts +201 -0
  534. package/src/inspector-section-registry.ts +221 -0
  535. package/src/instance-source-actions.ts +163 -0
  536. package/src/instanced-presentation.ts +164 -0
  537. package/src/js-heap.ts +71 -0
  538. package/src/key-actions.ts +91 -0
  539. package/src/keymap-presets.ts +401 -0
  540. package/src/layout-policy.ts +31 -0
  541. package/src/learn-links.ts +73 -0
  542. package/src/light-explorer-model.ts +134 -0
  543. package/src/live-canvas-frame.ts +55 -0
  544. package/src/live-document.ts +279 -0
  545. package/src/live-module-source.ts +230 -0
  546. package/src/live-session-registry.ts +220 -0
  547. package/src/live-transition.ts +633 -0
  548. package/src/manifest-project.ts +107 -0
  549. package/src/model-thumbnail.ts +523 -0
  550. package/src/native-selection-style.ts +202 -0
  551. package/src/object3d-document-write-policy.ts +137 -0
  552. package/src/packaged-runtime.ts +108 -0
  553. package/src/palettes/maya.palette.json +57 -0
  554. package/src/palettes/substance.palette.json +57 -0
  555. package/src/pasteboard-module.ts +81 -0
  556. package/src/performance-profiler.ts +367 -0
  557. package/src/performance-sources.ts +69 -0
  558. package/src/play-boot-phase.ts +145 -0
  559. package/src/presentation-surface.ts +248 -0
  560. package/src/project-adapter.ts +1143 -0
  561. package/src/project-asset-refresh.ts +26 -0
  562. package/src/project-declaration-refresh.ts +31 -0
  563. package/src/project-local-state.ts +118 -0
  564. package/src/project-manager.ts +243 -0
  565. package/src/project-module-changes.ts +163 -0
  566. package/src/project-module-split.ts +266 -0
  567. package/src/project-provenance.ts +115 -0
  568. package/src/project-ready.ts +42 -0
  569. package/src/project-session-reset.ts +67 -0
  570. package/src/project-shape.ts +68 -0
  571. package/src/project-tool-discovery.ts +66 -0
  572. package/src/project-tools.ts +107 -0
  573. package/src/project-work-types.ts +149 -0
  574. package/src/projection/three.ts +898 -0
  575. package/src/projection/types.ts +44 -0
  576. package/src/readiness.ts +113 -0
  577. package/src/reflection-probe-helper.ts +142 -0
  578. package/src/reported-play-state.ts +90 -0
  579. package/src/resolve-relative-specifier.ts +33 -0
  580. package/src/scene-document-plan.ts +315 -0
  581. package/src/scene-framing.ts +315 -0
  582. package/src/scene-live-open.ts +210 -0
  583. package/src/scene-view-fog.ts +89 -0
  584. package/src/scoped-game-css.ts +152 -0
  585. package/src/session-orphan-record.ts +193 -0
  586. package/src/session-tombstone.ts +126 -0
  587. package/src/settings/settings-provider.ts +82 -0
  588. package/src/settings-store.ts +345 -0
  589. package/src/shared-view-restore.ts +42 -0
  590. package/src/shell-store-door.ts +45 -0
  591. package/src/source-conflict.ts +122 -0
  592. package/src/spatial-handle-visuals.ts +332 -0
  593. package/src/stage-context.ts +341 -0
  594. package/src/stage-store-registry.ts +63 -0
  595. package/src/stale-chunk-recovery.ts +34 -0
  596. package/src/state-report-deferral.ts +73 -0
  597. package/src/storage/host-files-storage.ts +97 -0
  598. package/src/storage/http-storage.ts +174 -0
  599. package/src/storage/index.ts +75 -0
  600. package/src/storage/mem-storage.ts +149 -0
  601. package/src/storage/path-lock.ts +44 -0
  602. package/src/storage/paths.ts +26 -0
  603. package/src/storage/types.ts +116 -0
  604. package/src/stories/StoryComponentThumbnail.tsx +184 -0
  605. package/src/stories/StoryPreviewMount.tsx +306 -0
  606. package/src/stories/component-content-source.tsx +106 -0
  607. package/src/stories/component-states-source.ts +78 -0
  608. package/src/stories/compose-project-stories.ts +255 -0
  609. package/src/stories/pixi-story-model.ts +30 -0
  610. package/src/stories/prefabs-finder.ts +54 -0
  611. package/src/stories/prefabs-from-stories.ts +182 -0
  612. package/src/stories/project-story-discovery.ts +78 -0
  613. package/src/stories/project-story-regions.ts +24 -0
  614. package/src/stories/story-actions.ts +54 -0
  615. package/src/stories/story-capture-command.ts +124 -0
  616. package/src/stories/story-capture.ts +584 -0
  617. package/src/stories/story-declared-medium.ts +126 -0
  618. package/src/stories/story-discovery.ts +176 -0
  619. package/src/stories/story-document-openers.ts +36 -0
  620. package/src/stories/story-dom-runtime.ts +78 -0
  621. package/src/stories/story-grouping.ts +111 -0
  622. package/src/stories/story-lane.ts +88 -0
  623. package/src/stories/story-mount-turn.ts +27 -0
  624. package/src/stories/story-opener.ts +260 -0
  625. package/src/stories/story-pixi-preview.ts +408 -0
  626. package/src/stories/story-presentation.ts +215 -0
  627. package/src/stories/story-registry.ts +517 -0
  628. package/src/stories/story-three-preview.ts +807 -0
  629. package/src/stories/three-story-model.ts +96 -0
  630. package/src/story-three-preview-runtime.ts +56 -0
  631. package/src/surface-keyboard.ts +112 -0
  632. package/src/surface-state.ts +124 -0
  633. package/src/tab-bootstrap.js +628 -0
  634. package/src/tab-census.ts +198 -0
  635. package/src/tab-lifecycle-client.ts +196 -0
  636. package/src/theme-library.ts +844 -0
  637. package/src/theme-preference.ts +378 -0
  638. package/src/theme.css +3437 -0
  639. package/src/three-viewport/bone-selection-highlight.ts +119 -0
  640. package/src/three-viewport/camera-fit.ts +41 -0
  641. package/src/three-viewport/interactive-renderer.ts +132 -0
  642. package/src/three-viewport/selection-brackets.ts +310 -0
  643. package/src/three-viewport/selection-outline.ts +183 -0
  644. package/src/three-viewport/skeleton-helper.ts +61 -0
  645. package/src/three-viewport/source-color.ts +167 -0
  646. package/src/three-viewport/studio-environment.ts +96 -0
  647. package/src/three-viewport-presentation.ts +23 -0
  648. package/src/tool-contribution-play.ts +74 -0
  649. package/src/tool-loader.ts +1798 -0
  650. package/src/transform-mode-request.ts +57 -0
  651. package/src/transient-hint.ts +78 -0
  652. package/src/trigger-volume-helper.ts +116 -0
  653. package/src/ui-source/adapter-region-includes.ts +241 -0
  654. package/src/ui-source/ensure-import.ts +132 -0
  655. package/src/ui-source/file-region-resolver.ts +302 -0
  656. package/src/ui-source/inspect.ts +775 -0
  657. package/src/ui-source/oid-transform.ts +1845 -0
  658. package/src/ui-source/plan-csf-story.ts +196 -0
  659. package/src/ui-source/plan-extract-component.ts +421 -0
  660. package/src/ui-source/plan-fork-component.ts +621 -0
  661. package/src/ui-source/plan-named-style.ts +95 -0
  662. package/src/ui-source/plan-source-edit.ts +353 -0
  663. package/src/ui-source/r3f-contract-resolver.ts +327 -0
  664. package/src/ui-source/r3f-diagnostic-index.ts +165 -0
  665. package/src/ui-source/r3f-environment-binding.ts +133 -0
  666. package/src/ui-source/r3f-joint-binding.ts +251 -0
  667. package/src/ui-source/r3f-lod-binding.ts +91 -0
  668. package/src/ui-source/r3f-particle-binding.ts +235 -0
  669. package/src/ui-source/r3f-physics-binding.ts +483 -0
  670. package/src/ui-source/r3f-project-contracts.ts +444 -0
  671. package/src/ui-source/relative-import-specifier.ts +34 -0
  672. package/src/ui-source/reparent-guard.ts +544 -0
  673. package/src/ui-source/source-edit-request.ts +97 -0
  674. package/src/ui-source/source-write-backend.ts +618 -0
  675. package/src/ui-source/syntactic-prop-specs.ts +296 -0
  676. package/src/ui-source/tier-source-write-backend.ts +279 -0
  677. package/src/ui-source/ts-ast.ts +171 -0
  678. package/src/ui-source/utility-class-support.ts +169 -0
  679. package/src/ui-source/write-component-default.ts +133 -0
  680. package/src/ui-source/writer.ts +1989 -0
  681. package/src/viewport-activation-timings.ts +840 -0
  682. package/src/viewport-authoring-policy.ts +186 -0
  683. package/src/viewport-controls-hint.ts +57 -0
  684. package/src/viewport-door.ts +237 -0
  685. package/src/viewport-shading-boundary.ts +12 -0
  686. package/src/vite-error-surface.ts +141 -0
  687. package/src/wait-until.ts +37 -0
  688. package/src/workspace-areas.ts +156 -0
  689. package/src/workspace-aux-commands.ts +11 -0
  690. package/src/workspace-available-documents.ts +127 -0
  691. package/src/workspace-core-utilities.ts +31 -0
  692. package/src/workspace-document-ids.ts +59 -0
  693. package/src/workspace-document-registry.ts +612 -0
  694. package/src/workspace-document-restore.ts +145 -0
  695. package/src/workspace-host-commands.ts +141 -0
  696. package/src/workspace-persistence-gate.ts +40 -0
  697. package/src/workspace-play-utilities.ts +44 -0
  698. package/src/workspace-presets.ts +425 -0
  699. package/src/workspace-regions.ts +294 -0
  700. package/src/workspace-state-persistence.ts +536 -0
  701. package/src/workspace-static-panels.ts +73 -0
  702. package/src/workspace-status-registry.ts +121 -0
  703. package/src/workspace-style.ts +239 -0
  704. package/src/workspace-utility-commands.ts +74 -0
  705. package/src/workspace-utility-registry.ts +263 -0
  706. package/src/workspace-viewport-rect.ts +97 -0
  707. package/src/world-adoption.ts +115 -0
  708. package/src/world-document-routing.ts +104 -0
  709. package/vite-plugin-creation-site-write.ts +184 -0
  710. package/vite-plugin-creation-site.ts +80 -0
  711. package/vite-plugin-game-static.ts +303 -0
  712. package/vite-plugin-module-doorways.ts +336 -0
  713. package/vite-plugin-product-contributions.ts +197 -0
  714. package/vite-plugin-project-game-static.ts +125 -0
  715. package/vite-plugin-project-jsx-js.ts +65 -0
  716. package/vite-plugin-project-root-absolute-assets.ts +359 -0
  717. package/vite-plugin-shared-react.ts +420 -0
  718. package/vite-plugin-shared-three.ts +203 -0
  719. 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
+ }