@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,968 @@
1
+ /**
2
+ * The RECORDER for a repo-vendored game's own source.
3
+ *
4
+ * A vendored game is somebody else's code we ship at the ZERO UNRECORDED DIFF
5
+ * bar (`vendor/games/verify-unaltered.mjs`): every byte in the folder is in
6
+ * exactly one declared bucket of `vendor/games/<id>.UPSTREAM.lock`, and each
7
+ * bucket is hashed. That bar has never forbidden divergence — it forbids
8
+ * SILENT divergence. So an authored edit to a vendored game is not a special
9
+ * case to be refused; it is an edit whose RECORDER is the lock instead of the
10
+ * user's own git.
11
+ *
12
+ * That is the whole content of this module. `server/creation-site-write.ts`
13
+ * decides whether an edit may be written at all (ownership, containment, the
14
+ * checksum guard) and `src/creation-site-edit.ts` decides what the new bytes
15
+ * are; when those two say yes and the target happens to live inside a vendored
16
+ * game, this module performs the write AND the lock update as one operation, so
17
+ * the folder is never in a state the verifier would call an unrecorded diff.
18
+ *
19
+ * ## What it writes into the lock
20
+ *
21
+ * Nothing new: the existing `patches` bucket, whose contract is exactly what an
22
+ * authored edit needs — a unified diff plus the two shas, with the verifier
23
+ * REVERSE-APPLYING the diff and checking the result re-derives upstream. The
24
+ * recorded diff IS the authoring edit, so that leg keeps working by
25
+ * construction. The reconciliation is a pure function of the file's bytes:
26
+ *
27
+ * bytes === upstream → the path belongs in `sha256_manifest`, patch deleted
28
+ * bytes !== upstream → the path belongs in `patches`, with a fresh diff
29
+ *
30
+ * which is what makes UNDO symmetric for free. Undo hands the previous bytes
31
+ * back through the same `/__ingest-source/apply` route, the reconciliation runs
32
+ * again, and the last undo — the one that restores upstream's own bytes —
33
+ * removes the patch entry and puts the path back in the manifest. No direction
34
+ * flag, no separate "unrecord" path that could disagree with the recording one.
35
+ *
36
+ * A `host_added` file (the contract shim) has no upstream at all, so its record
37
+ * is a single sha and that is what gets refreshed.
38
+ *
39
+ * ## Why upstream's bytes are never fetched
40
+ *
41
+ * They are already in the folder, twice over: for a pristine file the on-disk
42
+ * bytes ARE upstream (the manifest's sha proves it), and for an already-patched
43
+ * file reverse-applying the recorded diff reproduces them (the verifier's own
44
+ * leg 2, run here for the same reason). So the recorder is hermetic — no
45
+ * network, no clone — and it REFUSES rather than guesses when either derivation
46
+ * disagrees with the lock, because a lock that already disagrees with its
47
+ * folder is not a base anything may record onto.
48
+ *
49
+ * ## The crash window, and the journal that closes it
50
+ *
51
+ * Two files must move together and no filesystem moves two files atomically.
52
+ * The order used here is: plan everything (any refusal happens before a byte
53
+ * moves) → write a ROLL-FORWARD JOURNAL naming the complete end state → write
54
+ * the artifacts → delete the journal. A process that dies mid-write leaves the
55
+ * journal, and {@link settleVendoredWrite} — called by every `/__ingest-source/*`
56
+ * handler before it reads or writes the game — completes it. The journal lives
57
+ * in `<game>/.vgai/`, the host-state directory the verifier already excludes and
58
+ * git already ignores, so it is never itself a diff.
59
+ *
60
+ * ## Why every path here is bytes, not text
61
+ *
62
+ * A vendored game's files are not all source. A level-based game's placed
63
+ * objects live in a binary level file — megabytes of it — so authoring one is a
64
+ * write to a BINARY file and this module has to
65
+ * record it like any other. Two measured facts shape the code below:
66
+ *
67
+ * - **UTF-8 is lossy and silent.** Round-tripping a 2.3 MB binary level file
68
+ * through a string turned 2,339,773 bytes into 3,169,557. Nothing throws;
69
+ * the file is simply a
70
+ * different file. So every read, write, hash and journal field here carries
71
+ * `Buffer`, and the only `utf8` left is on the lock and the patch text, which
72
+ * are genuinely text.
73
+ * - **`git diff` says nothing about binary unless asked.** Without `--binary`
74
+ * it emits `Binary files a/x and b/x differ` — 233 bytes of prose that
75
+ * `git apply -R` cannot reverse, which would record a patch the verifier
76
+ * could never check. With `--binary` it emits a GIT binary patch carrying
77
+ * BOTH directions (a forward and a reverse delta, ~4.8 KB for a 12-byte
78
+ * change in that 2.3 MB file) and `git apply -R` reverses it exactly.
79
+ *
80
+ * `--binary` is therefore passed unconditionally rather than behind a
81
+ * content-type test: measured on a text file it produces byte-identical output
82
+ * to leaving it off, so one code path serves both and there is no sniffing rule
83
+ * to get wrong. The verifier needs no counterpart change — it already copies
84
+ * bytes (`copyFileSync`), already hashes a `Buffer`, and `git apply -R` is the
85
+ * same tool on either kind of patch.
86
+ */
87
+
88
+ import { execFileSync } from 'node:child_process';
89
+ import { createHash, randomUUID } from 'node:crypto';
90
+ import {
91
+ existsSync,
92
+ mkdirSync,
93
+ mkdtempSync,
94
+ readdirSync,
95
+ readFileSync,
96
+ realpathSync,
97
+ renameSync,
98
+ rmSync,
99
+ statSync,
100
+ writeFileSync,
101
+ } from 'node:fs';
102
+ import { tmpdir } from 'node:os';
103
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
104
+
105
+ /**
106
+ * Where every lock lives, whatever the game is or how it is vendored — and,
107
+ * for a `source-tree` game, where its folder lives too. Exported because
108
+ * `creation-site-write.ts` resolves a vendored anchor against this same home
109
+ * (a repo-vendored module's creation site is recorded relative to it, by
110
+ * `vite-plugin-creation-site` through `ingestGameShadowRoots`), and two
111
+ * spellings of one path is how they drift apart.
112
+ */
113
+ export const LOCKS_DIR = ['vendor', 'games'];
114
+
115
+ /**
116
+ * Host-authored state inside a game folder — the same two names
117
+ * `verify-unaltered.mjs` excludes for every game. Writing here is never a diff,
118
+ * so it is never recorded.
119
+ */
120
+ const HOST_STATE_DIRS = ['.vgai', 'logs'];
121
+
122
+ /** `verify-unaltered.mjs`'s `DEFAULT_EXCLUDED_DIRS`, applied when a lock
123
+ * declares no `excluded` block of its own. */
124
+ const DEFAULT_EXCLUDED_DIRS = ['node_modules', 'dist', 'dist-ssr'];
125
+
126
+ /**
127
+ * One DEVIATION in a file's recorded diff.
128
+ *
129
+ * A patched file can carry more than one, applied at different times by
130
+ * different hands — `src/gameseq.js` carries a hand-written accessor-export
131
+ * deviation and, on top of it, an editor-authored anchor. The patch reverse-
132
+ * applies to upstream as a UNIT, so the two share one diff and one pair of
133
+ * shas; only the rationale is per-layer.
134
+ *
135
+ * `onto_sha256` is the file's content BEFORE this layer was applied, and it is
136
+ * what makes the stack reversible: an editor undo arrives as ordinary bytes,
137
+ * and the only way to know it removes the TOP layer rather than adding a third
138
+ * is that the incoming bytes hash to the top layer's own base.
139
+ */
140
+ interface WhyLayer {
141
+ why: string;
142
+ onto_sha256: string;
143
+ /** True when the recorder wrote this layer's prose, false/absent for a
144
+ * rationale a person wrote. The recorder may re-word its own and must never
145
+ * re-word a person's. */
146
+ generated?: boolean;
147
+ }
148
+
149
+ interface PatchSpec {
150
+ why: string;
151
+ /** Present only once a file carries MORE THAN ONE deviation, or once the
152
+ * recorder has added one of its own on top of an existing rationale — a
153
+ * single hand-written patch keeps the shape it has always had. */
154
+ why_layers?: WhyLayer[];
155
+ patch: string;
156
+ upstream_sha256: string;
157
+ patched_sha256: string;
158
+ }
159
+
160
+ interface VendorLock {
161
+ id: string;
162
+ kind?: string;
163
+ bundle_dir?: string;
164
+ excluded?: { dirs?: string[]; files?: string[]; why?: string };
165
+ patches?: Record<string, PatchSpec>;
166
+ host_added?: { why?: string; files?: Record<string, string> };
167
+ tracked_file_count?: number;
168
+ sha256_manifest: Record<string, string>;
169
+ }
170
+
171
+ /** A file that resolved inside a lock-recorded vendored game. */
172
+ export interface VendoredTarget {
173
+ readonly id: string;
174
+ /** Absolute path of `vendor/games/<id>.UPSTREAM.lock`. */
175
+ readonly lockPath: string;
176
+ /** Absolute path of the directory the lock's manifest is rooted at. */
177
+ readonly gameDir: string;
178
+ /** The file's POSIX path relative to `gameDir` — the key every bucket uses. */
179
+ readonly rel: string;
180
+ }
181
+
182
+ export type VendoredWriteResult =
183
+ | { readonly ok: true; readonly recorded: string }
184
+ | { readonly ok: false; readonly error: string };
185
+
186
+ /** Hashes BYTES, so it agrees with the verifier's `sha256(readFileSync(path))`
187
+ * for every file — text and binary alike. */
188
+ function sha256(bytes: Buffer): string {
189
+ return createHash('sha256').update(bytes).digest('hex');
190
+ }
191
+
192
+ /**
193
+ * Git's own heuristic: a NUL byte near the front means "not text". Used only to
194
+ * describe an edit in the lock, never to decide how it is recorded — that path
195
+ * is one code path for both kinds on purpose.
196
+ */
197
+ function looksBinary(bytes: Buffer): boolean {
198
+ return bytes.subarray(0, 8000).includes(0);
199
+ }
200
+
201
+ function posix(path: string): string {
202
+ return path.split(sep).join('/');
203
+ }
204
+
205
+ function isAtOrInside(parent: string, child: string): boolean {
206
+ const rel = relative(parent, child);
207
+ return rel === '' || (!rel.startsWith(`..${sep}`) && rel !== '..' && !isAbsolute(rel));
208
+ }
209
+
210
+ function readLock(lockPath: string): VendorLock {
211
+ return JSON.parse(readFileSync(lockPath, 'utf8')) as VendorLock;
212
+ }
213
+
214
+ /** The directory a lock's manifest is rooted at — `gameDirFor` in the verifier,
215
+ * which is the one place this correspondence has to stay true. */
216
+ function gameDirFor(lock: VendorLock, engineRoot: string): string | null {
217
+ if ((lock.kind ?? 'source-tree') === 'bundle') {
218
+ return lock.bundle_dir ? resolve(engineRoot, lock.bundle_dir) : null;
219
+ }
220
+ return resolve(engineRoot, ...LOCKS_DIR, lock.id);
221
+ }
222
+
223
+ /** One lock's IDENTITY — everything {@link findVendoredTarget} answers with,
224
+ * and nothing that a recorded write changes. */
225
+ interface VendoredRoot {
226
+ readonly id: string;
227
+ readonly lockPath: string;
228
+ /** Both spellings of the game's folder — see the symlink note below. */
229
+ readonly bases: readonly string[];
230
+ }
231
+
232
+ /**
233
+ * The root table, memoized per locks directory — because resolving it costs
234
+ * 1.1 MB of JSON and ~28 `realpathSync` calls, and the answer for the
235
+ * overwhelmingly common input is `null`.
236
+ *
237
+ * MEASURED on this repo's 28-lock estate: an uncached lookup of a path in a
238
+ * user's own project — every read and every write the editor makes, since a
239
+ * vendored game is the rare case — cost **3.83 ms**, all of it spent proving a
240
+ * negative. Cached, the same lookup is a string-prefix walk. The write point
241
+ * paid this per write; once the READ points settle too (which is why this
242
+ * memo exists) it would have been paid per edit gesture as well.
243
+ *
244
+ * WHAT IS CACHED IS ONLY THE IDENTITY — `id`, `lockPath`, and the folder the
245
+ * manifest is rooted at. Those come from `id`/`kind`/`bundle_dir`, which a
246
+ * recorded write never touches; the mutable half (`patches`, `sha256_manifest`)
247
+ * is re-read from disk by every caller that needs it, so a recorded write can
248
+ * never be served a stale lock through here.
249
+ *
250
+ * REVALIDATION is the locks directory's own `mtimeMs` plus its entry list —
251
+ * two syscalls. Adding, removing or renaming a lock changes both, and the
252
+ * recorder writes locks through `renameSync` INTO this directory, so an
253
+ * ordinary recorded write bumps the mtime and refreshes the table on its own.
254
+ * The one edit this cannot see is a lock's `kind`/`bundle_dir` being rewritten
255
+ * IN PLACE by hand, which relocates an existing game's folder — a repo-level
256
+ * change, not a runtime one, and it takes a dev-server restart like every
257
+ * other.
258
+ */
259
+ const rootTableByLocksDir = new Map<
260
+ string,
261
+ { readonly mtimeMs: number; readonly names: string; readonly roots: readonly VendoredRoot[] }
262
+ >();
263
+
264
+ function vendoredRoots(engineRoot: string): readonly VendoredRoot[] {
265
+ const locksDir = resolve(engineRoot, ...LOCKS_DIR);
266
+ let mtimeMs: number;
267
+ let names: string[];
268
+ try {
269
+ mtimeMs = statSync(locksDir).mtimeMs;
270
+ names = readdirSync(locksDir).filter((name) => name.endsWith('.UPSTREAM.lock'));
271
+ } catch {
272
+ rootTableByLocksDir.delete(locksDir);
273
+ return [];
274
+ }
275
+ const key = names.join('');
276
+ const cached = rootTableByLocksDir.get(locksDir);
277
+ if (cached && cached.mtimeMs === mtimeMs && cached.names === key) return cached.roots;
278
+
279
+ const roots: VendoredRoot[] = [];
280
+ for (const name of names) {
281
+ const lockPath = join(locksDir, name);
282
+ let lock: VendorLock;
283
+ try {
284
+ lock = readLock(lockPath);
285
+ } catch {
286
+ continue;
287
+ }
288
+ const gameDir = gameDirFor(lock, engineRoot);
289
+ if (!gameDir) continue;
290
+ // `absPath` arrives realpath-resolved (the containment check in
291
+ // `creation-site-write.ts` is realpath-based), while `engineRoot` — and so
292
+ // `gameDir` — is whatever spelling the server was constructed with. On a
293
+ // checkout reached through a symlink (macOS `/tmp` → `/private/tmp` is the
294
+ // everyday case) the two spellings of the SAME directory diverge, and a
295
+ // prefix miss does not refuse anything: the caller falls through to
296
+ // the plain unrecorded write, which is exactly the silent-drift failure
297
+ // this module exists to make impossible. So both spellings are kept.
298
+ roots.push({ id: lock.id, lockPath, bases: [...new Set([gameDir, realOrSelf(gameDir)])] });
299
+ }
300
+ rootTableByLocksDir.set(locksDir, { mtimeMs, names: key, roots });
301
+ return roots;
302
+ }
303
+
304
+ /**
305
+ * The lock-recorded vendored game whose folder contains `absPath` (or IS
306
+ * `absPath`), or `null` when no lock covers it — a user's own project, where
307
+ * their version control is the recorder and nothing here has anything to do.
308
+ *
309
+ * Pure: it reads locks and answers. The repair pass is
310
+ * {@link settleVendoredWrite}, deliberately a separate call so a lookup never
311
+ * has a side effect.
312
+ */
313
+ export function findVendoredTarget(absPath: string, engineRoot: string): VendoredTarget | null {
314
+ for (const root of vendoredRoots(engineRoot)) {
315
+ for (const base of root.bases) {
316
+ if (!isAtOrInside(base, absPath)) continue;
317
+ return {
318
+ id: root.id,
319
+ lockPath: root.lockPath,
320
+ gameDir: base,
321
+ rel: posix(relative(base, absPath)),
322
+ };
323
+ }
324
+ }
325
+ return null;
326
+ }
327
+
328
+ /** The realpath when the directory exists, the spelling itself when it does
329
+ * not — a missing dir cannot contain `absPath` either way. */
330
+ function realOrSelf(path: string): string {
331
+ try {
332
+ return realpathSync(path);
333
+ } catch {
334
+ return path;
335
+ }
336
+ }
337
+
338
+ // ────────────────────────────────────────────────────────────── the journal
339
+
340
+ interface PendingVendoredWrite {
341
+ readonly version: 2;
342
+ readonly gameFile: string;
343
+ /** Base64, because the journal is JSON and the game file may be binary — a
344
+ * string field here would silently re-encode 2.3 MB of level data. */
345
+ readonly gameFileBase64: string;
346
+ readonly lockPath: string;
347
+ readonly lockContent: string;
348
+ /** Absolute path of the patch file, when this write has one to write OR to
349
+ * delete; `patchContent === null` means delete. */
350
+ readonly patchPath: string | null;
351
+ readonly patchContent: string | null;
352
+ }
353
+
354
+ function journalPathFor(target: VendoredTarget): string {
355
+ return join(target.gameDir, '.vgai', 'vendored-lock-write.json');
356
+ }
357
+
358
+ function hostStateDir(target: VendoredTarget): string {
359
+ return join(target.gameDir, '.vgai');
360
+ }
361
+
362
+ /**
363
+ * Write `content` to `path` through a rename, staging the temporary file in the
364
+ * game's own host-state directory so a torn write is never visible AS A FILE to
365
+ * the verifier's directory walk (which excludes `.vgai/`) or to git.
366
+ */
367
+ function writeThroughRename(path: string, content: Buffer, stageDir: string): void {
368
+ mkdirSync(stageDir, { recursive: true });
369
+ mkdirSync(dirname(path), { recursive: true });
370
+ const staged = join(stageDir, `stage-${process.pid}-${randomUUID()}.tmp`);
371
+ writeFileSync(staged, content);
372
+ renameSync(staged, path);
373
+ }
374
+
375
+ /** Text artifacts — the lock and the patch — are genuinely text; only the game
376
+ * file itself may be binary. */
377
+ function writeTextThroughRename(path: string, content: string, stageDir: string): void {
378
+ writeThroughRename(path, Buffer.from(content, 'utf8'), stageDir);
379
+ }
380
+
381
+ function applyPending(pending: PendingVendoredWrite, stageDir: string): void {
382
+ if (pending.patchPath) {
383
+ if (pending.patchContent === null) rmSync(pending.patchPath, { force: true });
384
+ else writeTextThroughRename(pending.patchPath, pending.patchContent, stageDir);
385
+ }
386
+ writeTextThroughRename(pending.lockPath, pending.lockContent, stageDir);
387
+ writeThroughRename(pending.gameFile, Buffer.from(pending.gameFileBase64, 'base64'), stageDir);
388
+ }
389
+
390
+ /**
391
+ * Finish any write that died between its artifacts, so no caller ever reads or
392
+ * writes a vendored game whose lock and bytes disagree because of us.
393
+ *
394
+ * Roll-FORWARD only, and therefore idempotent: the journal names the complete
395
+ * end state, so re-writing all of it is always correct no matter how far the
396
+ * interrupted attempt got.
397
+ *
398
+ * Returns whether it actually rolled anything forward. That answer is not
399
+ * bookkeeping: settling REPLACES THE FILE'S BYTES, so a caller that serves
400
+ * modules (the dev server) has to invalidate what it had cached for that file,
401
+ * exactly as it does after a write of its own. `false` — the ordinary case —
402
+ * means nothing moved and there is nothing to invalidate.
403
+ */
404
+ export function settleVendoredWrite(target: VendoredTarget): boolean {
405
+ const journal = journalPathFor(target);
406
+ if (!existsSync(journal)) return false;
407
+ const pending = JSON.parse(readFileSync(journal, 'utf8')) as PendingVendoredWrite;
408
+ if (pending.version !== 2) {
409
+ // FAIL CLOSED, LOUDLY. A journal exists because a write may have died
410
+ // between the game's bytes and its lock; one this build cannot read means
411
+ // we do not know whether they agree, and every route calls this precisely so
412
+ // nothing plans, reads or writes against bytes in that state. Returning
413
+ // quietly — which is what this did — let the very next request act on a
414
+ // possibly half-written vendored game with no signal anywhere.
415
+ const message =
416
+ `${target.id}: ${journal} is a pending vendored write this build cannot settle ` +
417
+ `(journal version ${String(pending.version)}, expected 2). The game's bytes and its lock ` +
418
+ 'may disagree. Check the game with `node vendor/games/verify-unaltered.mjs`, then delete ' +
419
+ 'the journal once the tree is consistent.';
420
+ console.error(`[vendored-lock-recorder] ${message}`);
421
+ throw new Error(message);
422
+ }
423
+ applyPending(pending, hostStateDir(target));
424
+ rmSync(journal, { force: true });
425
+ return true;
426
+ }
427
+
428
+ // ─────────────────────────────────────────────────────────── diff plumbing
429
+
430
+ /**
431
+ * `git`, run in a throwaway directory OUTSIDE any working tree.
432
+ *
433
+ * The verifier reverse-applies recorded diffs with `git apply -R`, so the
434
+ * recorded diffs are produced by `git diff` — the same tool, at zero fuzz. A
435
+ * hand-rolled unified diff would be a second implementation of the format the
436
+ * gate reads, which is the one place a subtle disagreement would show up as a
437
+ * red gate nobody could explain.
438
+ */
439
+ function inScratch<T>(body: (dir: string) => T): T {
440
+ const dir = mkdtempSync(join(tmpdir(), 'vgai-vendored-record-'));
441
+ try {
442
+ return body(dir);
443
+ } finally {
444
+ rmSync(dir, { recursive: true, force: true });
445
+ }
446
+ }
447
+
448
+ /**
449
+ * `git`, forced to treat the scratch directory as OUTSIDE every repository —
450
+ * enforced, not assumed.
451
+ *
452
+ * `tmpdir()` is `TMPDIR`, and `TMPDIR` can point INSIDE a working tree (an
453
+ * agent worktree sets a short one because a checkout path can overrun the
454
+ * ~104-byte unix-socket limit). When it does, `git apply -R` resolves the
455
+ * patch's paths against the enclosing repo's TOPLEVEL instead of `cwd`, finds
456
+ * nothing to change, and EXITS 0 — measured: the reverse-apply returned the
457
+ * patched bytes unchanged, so `upstreamBytesOf` computed a sha that was not
458
+ * upstream's and every write to that game was refused with "the lock is not a
459
+ * base anything may record onto", which names the wrong thing entirely.
460
+ *
461
+ * `GIT_CEILING_DIRECTORIES` at the scratch dir's PARENT stops repository
462
+ * discovery before it can leave the scratch dir, so the same command behaves
463
+ * identically wherever `TMPDIR` points. (`--unsafe-paths` was measured NOT to
464
+ * fix it; a `git init` in the scratch dir does, at the cost of a second
465
+ * process.)
466
+ */
467
+ function gitInScratch(dir: string, args: readonly string[]): string {
468
+ return execFileSync('git', args, {
469
+ cwd: dir,
470
+ env: { ...process.env, GIT_CEILING_DIRECTORIES: dirname(dir) },
471
+ encoding: 'utf8',
472
+ maxBuffer: 1 << 28,
473
+ stdio: ['ignore', 'pipe', 'pipe'],
474
+ });
475
+ }
476
+
477
+ /** A unified diff taking `before` to `after` for `rel`, in the `a/`+`b/` shape
478
+ * the recorded patches already use, or `null` when the two are identical. */
479
+ function unifiedDiff(rel: string, before: Buffer, after: Buffer): string | null {
480
+ if (before.equals(after)) return null;
481
+ return inScratch((dir) => {
482
+ const a = join(dir, 'a', rel);
483
+ const b = join(dir, 'b', rel);
484
+ mkdirSync(dirname(a), { recursive: true });
485
+ mkdirSync(dirname(b), { recursive: true });
486
+ writeFileSync(a, before);
487
+ writeFileSync(b, after);
488
+ let out = '';
489
+ try {
490
+ // `--binary` is what makes this work for a binary level file: without
491
+ // it git emits only `Binary files … differ`, which reverse-applies to
492
+ // nothing. On text it changes the output not at all.
493
+ out = gitInScratch(dir, [
494
+ 'diff',
495
+ '--binary',
496
+ '--no-index',
497
+ '--no-prefix',
498
+ '--no-color',
499
+ '--',
500
+ `a/${rel}`,
501
+ `b/${rel}`,
502
+ ]);
503
+ } catch (error) {
504
+ // `git diff --no-index` exits 1 WHEN THERE IS A DIFFERENCE — the normal
505
+ // case here. The diff text is still on stdout; only an empty stdout is a
506
+ // real failure.
507
+ out = String((error as { stdout?: string }).stdout ?? '');
508
+ }
509
+ if (!out.trim()) throw new Error(`git produced no diff for ${rel}`);
510
+ if (/^Binary files .* differ$/m.test(out)) {
511
+ // Belt and braces: if this ever reappears, the recorded patch would be
512
+ // unreversible and the verifier would fail later, on somebody else's
513
+ // commit. Refuse here instead, where the cause is still visible.
514
+ throw new Error(
515
+ `git described the change to ${rel} only as "Binary files differ" — that cannot be ` +
516
+ 'reverse-applied, so it is not a record and will not be written',
517
+ );
518
+ }
519
+ return out;
520
+ });
521
+ }
522
+
523
+ /** The content reverse-applying `patchText` to `patched` yields — the verifier's
524
+ * own leg 2, returning the bytes instead of their digest. */
525
+ function reverseApply(rel: string, patched: Buffer, patchText: string): Buffer {
526
+ return inScratch((dir) => {
527
+ const target = join(dir, rel);
528
+ mkdirSync(dirname(target), { recursive: true });
529
+ writeFileSync(target, patched);
530
+ writeFileSync(join(dir, '.recorded.patch'), patchText, 'utf8');
531
+ gitInScratch(dir, ['apply', '-R', '--whitespace=nowarn', '.recorded.patch']);
532
+ return readFileSync(target);
533
+ });
534
+ }
535
+
536
+ /** `src/render.js` → `src-render.js.patch`, the convention already on disk. */
537
+ function patchFileNameFor(rel: string): string {
538
+ return `${rel.split('/').join('-')}.patch`;
539
+ }
540
+
541
+ /** Reuse a patch record's own path once it exists. For a new record, keep the
542
+ * readable convention unless another source path already owns that name; the
543
+ * digest suffix makes the collision resolution deterministic and injective. */
544
+ function patchPathFor(
545
+ lock: VendorLock,
546
+ target: VendoredTarget,
547
+ bucket: 'files' | 'patches',
548
+ ): string {
549
+ const lockDir = dirname(target.lockPath);
550
+ if (bucket === 'patches') return join(lockDir, lock.patches![target.rel]!.patch);
551
+ const directory = join(lockDir, `${target.id}.patches`);
552
+ const readable = patchFileNameFor(target.rel);
553
+ const relativeCandidate = posix(relative(lockDir, join(directory, readable)));
554
+ const occupied = new Set(Object.values(lock.patches ?? {}).map((spec) => spec.patch));
555
+ if (!occupied.has(relativeCandidate)) return join(directory, readable);
556
+ const suffix = sha256(Buffer.from(target.rel, 'utf8')).slice(0, 10);
557
+ return join(directory, readable.replace(/\.patch$/, `-${suffix}.patch`));
558
+ }
559
+
560
+ // ─────────────────────────────────────────────────────────── reconciliation
561
+
562
+ type Bucket = 'files' | 'patches' | 'host_added' | 'excluded' | 'undeclared';
563
+
564
+ function bucketOf(lock: VendorLock, rel: string): Bucket {
565
+ const top = rel.split('/')[0] ?? '';
566
+ const excludedDirs = new Set([
567
+ ...(lock.excluded?.dirs ?? DEFAULT_EXCLUDED_DIRS),
568
+ ...HOST_STATE_DIRS,
569
+ ]);
570
+ if (excludedDirs.has(top)) return 'excluded';
571
+ if ((lock.excluded?.files ?? []).includes(rel)) return 'excluded';
572
+ if (lock.sha256_manifest[rel] !== undefined) return 'files';
573
+ if (lock.patches?.[rel] !== undefined) return 'patches';
574
+ if (lock.host_added?.files?.[rel] !== undefined) return 'host_added';
575
+ return 'undeclared';
576
+ }
577
+
578
+ /**
579
+ * Put `key` back into a key-ordered map at the position it sorts to, leaving
580
+ * every other key's relative order alone.
581
+ *
582
+ * The manifests on disk are ASCII-sorted, and an undo that returns a path to
583
+ * the manifest should leave a one-line diff there rather than a re-ordered
584
+ * file. Appending would do that today and drift forever after.
585
+ */
586
+ function insertOrdered<T>(map: Record<string, T>, key: string, value: T): Record<string, T> {
587
+ const out: Record<string, T> = {};
588
+ let placed = false;
589
+ for (const [existing, existingValue] of Object.entries(map)) {
590
+ if (!placed && existing > key) {
591
+ out[key] = value;
592
+ placed = true;
593
+ }
594
+ out[existing] = existingValue;
595
+ }
596
+ if (!placed) out[key] = value;
597
+ return out;
598
+ }
599
+
600
+ /** The generated `why` for an editor-authored deviation. Names the edit in the
601
+ * terms a reviewer reads the lock in: what moved, where, and by whom. */
602
+ function describeEdit(
603
+ rel: string,
604
+ note: string | undefined,
605
+ before: Buffer,
606
+ after: Buffer,
607
+ ): string {
608
+ // Where the bytes moved, in the terms the file itself is read in: a line
609
+ // number for source, a byte offset for data. Naming a "line" in a 2.3 MB
610
+ // level archive would be a number nobody could look up.
611
+ const where = (() => {
612
+ if (looksBinary(before) || looksBinary(after)) {
613
+ let at = 0;
614
+ while (at < before.length && at < after.length && before[at] === after[at]) at++;
615
+ return `byte ${at} (binary; ${before.length} → ${after.length} bytes)`;
616
+ }
617
+ const a = before.toString('utf8').split('\n');
618
+ const b = after.toString('utf8').split('\n');
619
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
620
+ if (a[i] !== b[i]) return `${rel}:${i + 1}`;
621
+ }
622
+ return `${rel}:1`;
623
+ })();
624
+ // The note names the EDIT (which property, at which creation site); the
625
+ // derived location names where the bytes actually moved, which is not the same
626
+ // place — a property is often written a line or two below its `new`.
627
+ const what = note ? `${note}; ` : '';
628
+ return (
629
+ `EDITOR-AUTHORED (creation-site write-back): ${what}changed at ${where}. ` +
630
+ 'Written into the game’s own source by the editor at the moment of the edit, and recorded ' +
631
+ 'here in the same operation — the diff IS the authoring edit, so it reverse-applies to ' +
632
+ 'upstream by construction.'
633
+ );
634
+ }
635
+
636
+ /**
637
+ * The promise a SINGLE recorder-authored deviation may make, spelled once
638
+ * because it is both appended (when composing one layer) and stripped (when an
639
+ * older single-layer record becomes layer one of a stack, where it stops being
640
+ * true).
641
+ */
642
+ const UNDO_TO_UPSTREAM =
643
+ ' Undoing the edit in the editor removes this entry and returns the path to `sha256_manifest`.';
644
+
645
+ /**
646
+ * The `why` a stack of layers reads as — and the ONE place the undo promise is
647
+ * made, because that promise depends on how many layers there are.
648
+ *
649
+ * A single layer keeps the shape a lock has always had. A later layer is
650
+ * APPENDED, and deliberately not renumbered into a list: the prose already
651
+ * there may itself describe several deviations (`src/gameseq.js`'s does), and
652
+ * re-labelling it "(1)" produced a record that opened "2 layered deviations …
653
+ * (1) TWO layered deviations …". Leaving what someone wrote exactly as they
654
+ * wrote it and adding after it reads correctly however many they described.
655
+ *
656
+ * The tail states the real undo condition, which is the defect this exists to
657
+ * prevent: a `why` that called an accessor-export patch editor-authored and
658
+ * promised an undo straight back to `sha256_manifest` that could not happen
659
+ * while the accessor hunks remained.
660
+ */
661
+ function composeWhy(layers: readonly WhyLayer[]): string {
662
+ const [first, ...rest] = layers;
663
+ if (!first) {
664
+ throw new Error(
665
+ 'a recorded patch always has at least one deviation to explain — a stack that emptied ' +
666
+ 'means the file is back to upstream, which removes the record rather than re-writing it',
667
+ );
668
+ }
669
+ if (rest.length === 0) return first.generated ? `${first.why}${UNDO_TO_UPSTREAM}` : first.why;
670
+ const added = rest.map((layer) => `AND, ON TOP OF THAT: ${layer.why}`).join(' ');
671
+ return (
672
+ `${first.why} ${added} These deviations were recorded separately and share one diff (the ` +
673
+ 'patch reverse-applies to upstream as a unit). Undoing the last edit in the editor removes ' +
674
+ 'only its own layer; the path returns to `sha256_manifest` only when EVERY deviation above ' +
675
+ 'is gone.'
676
+ );
677
+ }
678
+
679
+ /**
680
+ * The layer stack a patch entry already carries.
681
+ *
682
+ * A lock written before layering — or by hand — has a `why` and no `why_layers`,
683
+ * and that `why` is layer one: it describes the whole deviation as it stands,
684
+ * applied onto upstream. Seeding it that way is what lets a person's rationale
685
+ * survive the next editor edit intact, which is the whole point.
686
+ */
687
+ function existingLayers(spec: PatchSpec | undefined, upstreamSha: string): WhyLayer[] {
688
+ if (!spec) return [];
689
+ if (spec.why_layers && spec.why_layers.length > 0) return spec.why_layers.map((l) => ({ ...l }));
690
+ // A single-layer record the recorder itself wrote ends with a promise that is
691
+ // only true while it IS the only layer. Strip it here — on an EXACT match with
692
+ // our own sentence, so a person's prose is never touched — and let
693
+ // `composeWhy` re-add or replace it from the stack's real shape.
694
+ return spec.why.endsWith(UNDO_TO_UPSTREAM.trimStart())
695
+ ? [
696
+ {
697
+ why: spec.why.slice(0, -UNDO_TO_UPSTREAM.trimStart().length).trimEnd(),
698
+ onto_sha256: upstreamSha,
699
+ generated: true,
700
+ },
701
+ ]
702
+ : [{ why: spec.why, onto_sha256: upstreamSha }];
703
+ }
704
+
705
+ interface Reconciled {
706
+ readonly lock: VendorLock;
707
+ readonly patchPath: string | null;
708
+ readonly patchContent: string | null;
709
+ readonly recorded: string;
710
+ }
711
+
712
+ /**
713
+ * The buckets with no upstream counterpart to diff against.
714
+ *
715
+ * `host_added` is a file WE ship (the contract shim), so its record is one sha
716
+ * and re-hashing it is the whole update. The other two are not records at all:
717
+ * a path the lock has never heard of is ALREADY an unrecorded diff, and a path
718
+ * the lock excludes is outside the estate's accounting entirely — writing
719
+ * either through the recorder would be claiming an accounting that does not
720
+ * exist, so both refuse by name.
721
+ */
722
+ function reconcileWithoutUpstream(
723
+ next: VendorLock,
724
+ target: VendoredTarget,
725
+ current: Buffer,
726
+ after: Buffer,
727
+ bucket: Exclude<Bucket, 'files' | 'patches'>,
728
+ ): Reconciled {
729
+ const rel = target.rel;
730
+ if (bucket === 'undeclared') {
731
+ throw new Error(
732
+ `${rel} is not declared in ${target.id}'s lock — a file the lock does not know about ` +
733
+ 'is already an unrecorded diff, so the editor will not add a second one on top of it',
734
+ );
735
+ }
736
+ if (bucket === 'excluded') {
737
+ throw new Error(`${rel} is excluded from ${target.id}'s lock and needs no record`);
738
+ }
739
+ const recorded = next.host_added?.files?.[rel];
740
+ if (!recorded || sha256(current) !== recorded) {
741
+ throw new Error(
742
+ `${rel} does not match the sha256 ${target.id}'s host_added record claims — the file ` +
743
+ 'already changed outside the recorder, so this edit will not absorb that divergence',
744
+ );
745
+ }
746
+ const files = { ...(next.host_added?.files ?? {}) };
747
+ files[rel] = sha256(after);
748
+ next.host_added = { ...(next.host_added ?? {}), files };
749
+ return {
750
+ lock: next,
751
+ patchPath: null,
752
+ patchContent: null,
753
+ recorded: `${target.id}: host-added ${rel} re-hashed in the lock`,
754
+ };
755
+ }
756
+
757
+ /**
758
+ * Upstream's own bytes for a file that is on disk right now, derived from the
759
+ * folder itself — no network, no clone.
760
+ *
761
+ * Both derivations are CHECKED against the lock, and a disagreement throws
762
+ * rather than proceeds: recording onto a base that already diverges from its
763
+ * lock would launder somebody else's unrecorded diff into a sanctioned one,
764
+ * which is the one failure mode that would make this whole lane worse than the
765
+ * refusal it replaces.
766
+ */
767
+ function upstreamBytesOf(
768
+ lock: VendorLock,
769
+ target: VendoredTarget,
770
+ current: Buffer,
771
+ bucket: 'files' | 'patches',
772
+ ): Buffer {
773
+ const rel = target.rel;
774
+ if (bucket === 'files') {
775
+ if (sha256(current) !== lock.sha256_manifest[rel]) {
776
+ throw new Error(
777
+ `${rel} does not match the sha256 ${target.id}'s lock records for it — the folder ` +
778
+ 'already diverges from its own lock, so nothing may be recorded onto it',
779
+ );
780
+ }
781
+ return current;
782
+ }
783
+ const spec = lock.patches![rel]!;
784
+ const patchPath = join(dirname(target.lockPath), spec.patch);
785
+ if (!existsSync(patchPath)) {
786
+ throw new Error(`${target.id}'s lock points at a missing patch file for ${rel}`);
787
+ }
788
+ if (sha256(current) !== spec.patched_sha256) {
789
+ throw new Error(
790
+ `${rel} does not match the \`patched_sha256\` ${target.id}'s lock records for it — ` +
791
+ 'the folder already diverges from its own lock',
792
+ );
793
+ }
794
+ const upstream = reverseApply(rel, current, readFileSync(patchPath, 'utf8'));
795
+ if (sha256(upstream) !== spec.upstream_sha256) {
796
+ throw new Error(
797
+ `${target.id}'s recorded patch for ${rel} does not reverse-apply to the upstream sha ` +
798
+ 'it claims — the lock is not a base anything may record onto',
799
+ );
800
+ }
801
+ return upstream;
802
+ }
803
+
804
+ /**
805
+ * The lock as it must read once `after` is the file's content — the pure half,
806
+ * so a test drives exactly what the server does.
807
+ */
808
+ export function reconcileLock(
809
+ lock: VendorLock,
810
+ target: VendoredTarget,
811
+ current: Buffer,
812
+ after: Buffer,
813
+ note: string | undefined,
814
+ ): Reconciled {
815
+ const rel = target.rel;
816
+ const next: VendorLock = JSON.parse(JSON.stringify(lock)) as VendorLock;
817
+ const bucket = bucketOf(lock, rel);
818
+ if (bucket !== 'files' && bucket !== 'patches') {
819
+ return reconcileWithoutUpstream(next, target, current, after, bucket);
820
+ }
821
+
822
+ const upstream = upstreamBytesOf(lock, target, current, bucket);
823
+ const patchFile = patchPathFor(lock, target, bucket);
824
+
825
+ if (sha256(after) === sha256(upstream)) {
826
+ // Back to upstream's own bytes: the deviation is gone, so its record goes
827
+ // with it and the path returns to the manifest.
828
+ if (next.patches) {
829
+ delete next.patches[rel];
830
+ if (Object.keys(next.patches).length === 0) delete next.patches;
831
+ }
832
+ next.sha256_manifest = insertOrdered(next.sha256_manifest, rel, sha256(upstream));
833
+ syncTrackedCount(lock, next);
834
+ return {
835
+ lock: next,
836
+ patchPath: bucket === 'patches' ? patchFile : null,
837
+ patchContent: null,
838
+ recorded: `${target.id}: ${rel} is upstream’s own bytes again — its patch record removed`,
839
+ };
840
+ }
841
+
842
+ const patchText = unifiedDiff(rel, upstream, after);
843
+ if (patchText === null) throw new Error(`${rel} produced no diff against upstream`);
844
+
845
+ // LAYERING, under one rule: THE RECORDER OWNS AT MOST ONE LAYER — the top one
846
+ // — and never touches anything below it.
847
+ //
848
+ // - Over a rationale a person wrote, an editor edit ADDS a layer. Replacing
849
+ // it was the shipped defect: it called an accessor-export patch
850
+ // editor-authored and promised an undo straight back to `sha256_manifest`
851
+ // that could not happen while the accessor hunks remained.
852
+ // - Over the recorder's OWN layer, a further editor edit REPLACES it, keeping
853
+ // that layer's `onto_sha256`. A second drag of the same robot is not a
854
+ // second deviation, and a `why` that grew a paragraph per gesture would be
855
+ // unreadable within a session.
856
+ // - Bytes that hash to the top layer's own base are an UNDO of that layer:
857
+ // pop it, and the rationale underneath comes back exactly as it was.
858
+ const layers = existingLayers(lock.patches?.[rel], sha256(upstream));
859
+ const top = layers[layers.length - 1];
860
+ const undoingTop = top !== undefined && sha256(after) === top.onto_sha256;
861
+ const fresh: WhyLayer = {
862
+ why: describeEdit(rel, note, upstream, after),
863
+ // The base is the state before the recorder's layer BEGAN, not before this
864
+ // particular gesture — otherwise re-dragging an object would move the undo
865
+ // target forward and the first edit could never be popped.
866
+ onto_sha256: top?.generated ? top.onto_sha256 : sha256(current),
867
+ generated: true,
868
+ };
869
+ const nextLayers: WhyLayer[] = undoingTop
870
+ ? layers.slice(0, -1)
871
+ : top?.generated
872
+ ? [...layers.slice(0, -1), fresh]
873
+ : [...layers, fresh];
874
+
875
+ const spec: PatchSpec = {
876
+ why: composeWhy(nextLayers),
877
+ // The array is the machine-readable half and only earns its place once
878
+ // there is something a single `why` cannot express.
879
+ ...(nextLayers.length > 1 ? { why_layers: nextLayers } : {}),
880
+ patch: posix(relative(dirname(target.lockPath), patchFile)),
881
+ upstream_sha256: sha256(upstream),
882
+ patched_sha256: sha256(after),
883
+ };
884
+ next.patches = { ...(next.patches ?? {}) };
885
+ next.patches[rel] = spec;
886
+ if (bucket === 'files') {
887
+ delete next.sha256_manifest[rel];
888
+ syncTrackedCount(lock, next);
889
+ }
890
+ return {
891
+ lock: next,
892
+ patchPath: patchFile,
893
+ patchContent: patchText,
894
+ recorded: undoingTop
895
+ ? `${target.id}: ${rel}'s editor-authored layer removed; ${nextLayers.length} deviation(s) still recorded`
896
+ : `${target.id}: ${rel} recorded as a patch (${spec.patch})`,
897
+ };
898
+ }
899
+
900
+ /** Keep `tracked_file_count` meaning what it means — the manifest's length —
901
+ * and leave it alone when it never meant that in this lock. */
902
+ function syncTrackedCount(before: VendorLock, after: VendorLock): void {
903
+ if (before.tracked_file_count === Object.keys(before.sha256_manifest).length) {
904
+ after.tracked_file_count = Object.keys(after.sha256_manifest).length;
905
+ }
906
+ }
907
+
908
+ /** The 2-space shape every lock on disk already has, with the trailing newline
909
+ * a text file gets. An emptied `patches` was DELETED by then, so the key
910
+ * disappears from the serialized lock rather than becoming `{}`. */
911
+ function serializeLock(lock: VendorLock): string {
912
+ return `${JSON.stringify(lock, null, 2)}\n`;
913
+ }
914
+
915
+ // ──────────────────────────────────────────────────────────────── the write
916
+
917
+ /**
918
+ * Write `after` into a vendored game's file AND update the game's lock, as one
919
+ * journalled operation.
920
+ *
921
+ * Every refusal happens before a byte moves — that is the point of planning the
922
+ * whole end state first — and every refusal names what is wrong with the lock
923
+ * or the folder, never a generic failure.
924
+ */
925
+ export function writeRecordedVendoredFile(
926
+ target: VendoredTarget,
927
+ after: Buffer,
928
+ note?: string,
929
+ ): VendoredWriteResult {
930
+ const absFile = join(target.gameDir, ...target.rel.split('/'));
931
+ let plan: Reconciled;
932
+ let current: Buffer;
933
+ try {
934
+ current = readFileSync(absFile);
935
+ plan = reconcileLock(readLock(target.lockPath), target, current, after, note);
936
+ } catch (error) {
937
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
938
+ }
939
+
940
+ const pending: PendingVendoredWrite = {
941
+ version: 2,
942
+ gameFile: absFile,
943
+ gameFileBase64: after.toString('base64'),
944
+ lockPath: target.lockPath,
945
+ lockContent: serializeLock(plan.lock),
946
+ patchPath: plan.patchPath,
947
+ patchContent: plan.patchContent,
948
+ };
949
+ const stage = hostStateDir(target);
950
+ let journalWritten = false;
951
+ try {
952
+ writeTextThroughRename(journalPathFor(target), JSON.stringify(pending, null, 2), stage);
953
+ journalWritten = true;
954
+ applyPending(pending, stage);
955
+ rmSync(journalPathFor(target), { force: true });
956
+ } catch (error) {
957
+ if (journalWritten) {
958
+ return {
959
+ ok: true,
960
+ recorded:
961
+ `${target.id}: the complete recorder state is committed to its roll-forward journal; ` +
962
+ 'the next source access will settle the pending artifacts',
963
+ };
964
+ }
965
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
966
+ }
967
+ return { ok: true, recorded: plan.recorded };
968
+ }