@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,1479 @@
1
+ /**
2
+ * `planCreationSiteEdit` — the PURE plan for "write this property edit
3
+ * into the game's own source at the line that constructed the object".
4
+ *
5
+ * The read half: a serve-time transform stamps every direct
6
+ * `new` in a served project module, and a host-side `WeakMap` answers "which
7
+ * `file:line` built this live object?" (`creation-site-registry.ts`). This
8
+ * module is the write half's decision-maker. It takes the anchor, the source
9
+ * bytes, the value in force BEFORE the edit and the value the user produced,
10
+ * and returns either exact new bytes or a NAMED refusal — never a guess.
11
+ *
12
+ * ─────────────────────────────────────────────────────────────────────────────
13
+ * WHY THIS IS NOT A THREE.JS SIGNATURE TABLE, AND MUST NEVER BECOME ONE
14
+ *
15
+ * The tempting way to write `new THREE.DirectionalLight(0xffffff, 2)` back is a
16
+ * table saying "argument 1 of DirectionalLight is `intensity`". That table is
17
+ * fabrication with extra steps: it is unverifiable at the edit, it is wrong for
18
+ * every constructor nobody wrote a row for, and when it is wrong it silently
19
+ * corrupts someone else's game. This module knows NOTHING about three.js.
20
+ *
21
+ * Two rules replace that knowledge, and they do different jobs. The first
22
+ * decides WHICH literal may be written; the second decides WHETHER writing it
23
+ * is still true. Both must hold.
24
+ *
25
+ * 1. NAME ADDRESSING — the only way a literal ever becomes a candidate. The
26
+ * SOURCE ITSELF must name the property: `sun.position.set(…)` contains the
27
+ * word `position`; `new THREE.MeshBasicMaterial({ opacity: 0.2 })` contains
28
+ * the word `opacity`. A BARE POSITIONAL ARGUMENT NAMES NOTHING AND IS NEVER
29
+ * A CANDIDATE, no matter what its value happens to be. `scanConstructor`'s
30
+ * comment carries the three real corruptions that rule was bought with.
31
+ *
32
+ * 2. THE BASELINE-EQUALITY BACKSTOP. A candidate may be rewritten only when
33
+ * its CURRENT value equals the value in force on the live object. That
34
+ * subsumes an unbounded amount of static analysis this module deliberately
35
+ * does not do:
36
+ * - a later `sun.position.copy(target)` we never modelled? then the
37
+ * literal is not what is in force, so we refuse;
38
+ * - the object escaped into a function that mutated it? same;
39
+ * - the simulation animates the value every frame? same.
40
+ * Its contrapositive is the honest claim this feature makes: when we DO
41
+ * write, the literal we replace is demonstrably the value the user was
42
+ * looking at, so replacing it produces what they dragged to.
43
+ *
44
+ * NOTE WHAT RULE 2 CANNOT DO, because an earlier revision of this file got it
45
+ * wrong and shipped a corruption: it cannot tell a right slot from a wrong one.
46
+ * Equality is evidence about the VALUE, never about which slot means what — and
47
+ * when a coincidence is what put the two in agreement, the coincidence IS the
48
+ * equality, so the backstop nods it through. That is precisely why rule 1 is a
49
+ * gate and not a preference: rule 2 defends the write's TRUTH, and only rule 1
50
+ * defends its TARGET.
51
+ *
52
+ * Everything else here — receiver resolution, the enclosing-function walk,
53
+ * multiplicity, per-component homes — exists to FIND candidates under rule 1.
54
+ *
55
+ * ─────────────────────────────────────────────────────────────────────────────
56
+ * BYTE-IDENTITY ON REFUSAL. Every refusal returns `newSource === prevSource`,
57
+ * identity-equal, and `changed: false`. A caller that writes on `changed` can
58
+ * never be tricked into a no-op write, and the file a refused gesture touched is
59
+ * bit-for-bit the file it started as. (`creation-site-edit.test.ts` asserts this
60
+ * over every refusal branch — the reparent-guard precedent.)
61
+ *
62
+ * ONE TRANSACTION. Every rule is decided before the first byte moves, and an
63
+ * accepted plan is ONE new whole-file string covering every component the
64
+ * gesture touched. The caller turns that into one history transaction pairing
65
+ * the live mutation with the source write, so undo restores both.
66
+ */
67
+
68
+ import ts from 'typescript';
69
+ import type { CreationSite } from './creation-site-registry';
70
+ import { lineColToOffset } from './ui-source/oid-transform';
71
+
72
+ /** Float slack for "is this literal the value in force". Gizmo drags and the
73
+ * quaternion→Euler round trip both land a few ULPs off an authored decimal. */
74
+ const EPS = 1e-6;
75
+
76
+ /** What a channel's value looks like crossing this seam. Vector channels arrive
77
+ * as three numbers; everything else is one scalar. */
78
+ export type ChannelValue = number | boolean | string | readonly number[];
79
+
80
+ /**
81
+ * The live sub-object that actually OWNS a property, and therefore whose
82
+ * creation site the edit must be anchored to.
83
+ *
84
+ * `material.color` is the case that forces this to exist: the mesh was built by
85
+ * `new THREE.Mesh(geometry, gridMaterial)`, but the colour lives on the
86
+ * MATERIAL, built by its own `new THREE.MeshBasicMaterial({ color: … })`
87
+ * elsewhere. Anchoring a colour edit to the mesh's line would look for a `color`
88
+ * the mesh's constructor never mentions — and the only ways out of that are
89
+ * refusing a perfectly writable edit or inventing a link. Asking the caller for
90
+ * the material's OWN anchor is neither.
91
+ */
92
+ export type ChannelOwner = 'self' | 'material';
93
+
94
+ export type ChannelKind =
95
+ | 'vector3'
96
+ | 'vector2'
97
+ | 'euler'
98
+ | 'color'
99
+ | 'number'
100
+ | 'boolean'
101
+ | 'string';
102
+
103
+ export interface PropertyChannel {
104
+ /** Which live object's creation site this property is anchored to. */
105
+ readonly owner: ChannelOwner;
106
+ /** The RECEIVER-relative member path as it is spelled in source
107
+ * (`position`, `intensity`, `castShadow`). */
108
+ readonly member: string;
109
+ readonly kind: ChannelKind;
110
+ /**
111
+ * Receiver-relative member paths that each hold ONE component of this channel
112
+ * DIRECTLY, in source order — a SECOND spelling of the same storage, not a
113
+ * second property.
114
+ *
115
+ * Pixi is why this exists: `Container.x` is a getter/setter over
116
+ * `this.position.x`, so `hud.x = 16` and `hud.position.set(16, 16)` are the
117
+ * same value written two ways, and both are ordinary idiom in an unmodified
118
+ * game. Modelling them as two channels would be the two-parallel-lists defect
119
+ * `IngestSourcePersistence.gate`'s comment records; modelling the alias as a
120
+ * spelling keeps ONE channel, one baseline, and one last-writer-wins ordering
121
+ * over every literal found either way.
122
+ *
123
+ * Absent for every three channel: `Object3D` has no `.x`.
124
+ */
125
+ readonly componentMembers?: readonly string[];
126
+ }
127
+
128
+ /**
129
+ * The editor's inspector/transform property paths → what they are in source.
130
+ *
131
+ * This is a DATA table over paths the ingest adapter already reads and writes
132
+ * (`ThreeAuthoringAdapter.inspector.properties`), not a vocabulary invented
133
+ * here: every key is a path some existing editor surface already produces, and
134
+ * a path absent from this table is refused by name rather than guessed at.
135
+ */
136
+ export const CREATION_SITE_CHANNELS: Readonly<Record<string, PropertyChannel>> = {
137
+ position: { owner: 'self', member: 'position', kind: 'vector3' },
138
+ rotation: { owner: 'self', member: 'rotation', kind: 'euler' },
139
+ scale: { owner: 'self', member: 'scale', kind: 'vector3' },
140
+ visible: { owner: 'self', member: 'visible', kind: 'boolean' },
141
+ name: { owner: 'self', member: 'name', kind: 'string' },
142
+ 'material.color': { owner: 'material', member: 'color', kind: 'color' },
143
+ 'light.color': { owner: 'self', member: 'color', kind: 'color' },
144
+ 'light.intensity': { owner: 'self', member: 'intensity', kind: 'number' },
145
+ 'light.distance': { owner: 'self', member: 'distance', kind: 'number' },
146
+ 'camera.fov': { owner: 'self', member: 'fov', kind: 'number' },
147
+ 'camera.near': { owner: 'self', member: 'near', kind: 'number' },
148
+ 'camera.far': { owner: 'self', member: 'far', kind: 'number' },
149
+ 'shadow.cast': { owner: 'self', member: 'castShadow', kind: 'boolean' },
150
+ 'shadow.receive': { owner: 'self', member: 'receiveShadow', kind: 'boolean' },
151
+ };
152
+
153
+ /**
154
+ * The CANVAS surface's table — the same contract, a different vocabulary.
155
+ *
156
+ * It is a SEPARATE table rather than extra rows because the same editor path
157
+ * means a different thing on each surface: `position` is a three-component
158
+ * vector on a `THREE.Object3D` and a two-component one on a `PIXI.Container`,
159
+ * and `rotation` is an Euler triple there and a scalar in radians here. Merging
160
+ * them would need a per-row surface tag on every row, which is the same fork
161
+ * wearing a field.
162
+ *
163
+ * Every key is a path the canvas adapter already produces — the three
164
+ * `TransformChannel`s plus the reflected inspector rows
165
+ * (`AuthoringAdapter2D.properties`) — so nothing here is a vocabulary invented
166
+ * at the write path.
167
+ *
168
+ * `scale` has no `componentMembers` on purpose: Pixi has `sprite.x` but no
169
+ * `sprite.scaleX`, so listing one would be a spelling no game can contain.
170
+ */
171
+ export const PIXI_CREATION_SITE_CHANNELS: Readonly<Record<string, PropertyChannel>> = {
172
+ position: { owner: 'self', member: 'position', kind: 'vector2', componentMembers: ['x', 'y'] },
173
+ rotation: { owner: 'self', member: 'rotation', kind: 'number' },
174
+ scale: { owner: 'self', member: 'scale', kind: 'vector2' },
175
+ visible: { owner: 'self', member: 'visible', kind: 'boolean' },
176
+ // The editor's `name` row is Pixi's `label` (`AuthoringAdapter2D.set` maps it).
177
+ name: { owner: 'self', member: 'label', kind: 'string' },
178
+ alpha: { owner: 'self', member: 'alpha', kind: 'number' },
179
+ tint: { owner: 'self', member: 'tint', kind: 'color' },
180
+ };
181
+
182
+ /** Babylon's native imperative transform/property spellings. The editor paths
183
+ * stay universal; only this substrate table says how those paths are written
184
+ * in Babylon source. */
185
+ export const BABYLON_CREATION_SITE_CHANNELS: Readonly<Record<string, PropertyChannel>> = {
186
+ position: { owner: 'self', member: 'position', kind: 'vector3' },
187
+ rotation: { owner: 'self', member: 'rotation', kind: 'euler' },
188
+ scale: { owner: 'self', member: 'scaling', kind: 'vector3' },
189
+ name: { owner: 'self', member: 'name', kind: 'string' },
190
+ visible: { owner: 'self', member: 'isVisible', kind: 'boolean' },
191
+ visibility: { owner: 'self', member: 'visibility', kind: 'number' },
192
+ };
193
+
194
+ /** Which RENDER SURFACE's vocabulary a request speaks. Not provenance: an
195
+ * ingested Pixi game and a first-party canvas world answer identically here,
196
+ * and a three world differs because `Object3D` is a different shape. */
197
+ export type CreationSiteSurface = 'three' | 'pixi' | 'babylon';
198
+
199
+ /** Authority carried by a creation-site write. Ordinary gestures address one
200
+ * live object; the component-default gesture deliberately addresses the source
201
+ * site itself and therefore every object constructed there. */
202
+ export type CreationSiteWriteScope = 'instance' | 'creation-site';
203
+
204
+ /** The channel for an editor property path on one surface, or `null` when this
205
+ * seam has no source expression for it. */
206
+ export function channelFor(
207
+ property: string,
208
+ surface: CreationSiteSurface = 'three',
209
+ ): PropertyChannel | null {
210
+ const table =
211
+ surface === 'pixi'
212
+ ? PIXI_CREATION_SITE_CHANNELS
213
+ : surface === 'babylon'
214
+ ? BABYLON_CREATION_SITE_CHANNELS
215
+ : CREATION_SITE_CHANNELS;
216
+ return table[property] ?? null;
217
+ }
218
+
219
+ export interface CreationSiteEditRequest {
220
+ /** The anchor the registry reported for the property's OWNING object. */
221
+ readonly site: CreationSite;
222
+ /**
223
+ * How many live objects have been recorded at this anchor. `1` is the only
224
+ * count a single-object edit may be written at — see {@link multiplicityRefusal}.
225
+ */
226
+ readonly instances: number;
227
+ /** Absent means the ordinary single-object gesture. `creation-site` is used
228
+ * only by the explicit Apply-to-component confirmation, whose UI names the
229
+ * full affected instance count before this request exists. */
230
+ readonly writeScope?: CreationSiteWriteScope;
231
+ /** An editor property path — a key of the surface's channel table. */
232
+ readonly property: string;
233
+ /** Which surface's vocabulary {@link CreationSiteEditRequest.property} is in.
234
+ * Absent ⇒ `three`, which is what every caller predating the canvas lane
235
+ * meant. */
236
+ readonly surface?: CreationSiteSurface;
237
+ /** The value in force on the live object BEFORE this gesture. */
238
+ readonly baseline: ChannelValue;
239
+ /** The value the gesture produced. */
240
+ readonly next: ChannelValue;
241
+ }
242
+
243
+ export interface CreationSiteEditPlan {
244
+ readonly changed: boolean;
245
+ readonly prevSource: string;
246
+ /** Identity-equal to `prevSource` whenever `changed` is false. */
247
+ readonly newSource: string;
248
+ /** Present iff `!changed` — the named reason, per the never-fabricate rule. */
249
+ readonly reason?: string;
250
+ /** Present iff `changed` — a one-line description for the history label. */
251
+ readonly wrote?: string;
252
+ }
253
+
254
+ function refuse(source: string, reason: string): CreationSiteEditPlan {
255
+ return { changed: false, prevSource: source, newSource: source, reason };
256
+ }
257
+
258
+ /**
259
+ * The refusal for a shared creation site, stated with the count that makes it
260
+ * true. SimCity's `const tile = new Tile(x, y)` builds all 256 tiles from one
261
+ * line, so a per-tile edit has nowhere honest to go: writing that line would
262
+ * move every tile, and writing it "just for this one" is a lie about what the
263
+ * source says. The count comes from the registry, which is the only thing that
264
+ * knows it.
265
+ */
266
+ export function multiplicityRefusal(instances: number): string {
267
+ return (
268
+ `this creation site constructs ${instances} objects; ` +
269
+ 'a per-object edit cannot be written there'
270
+ );
271
+ }
272
+
273
+ // ───────────────────────────────────────────────────────── value normalization
274
+
275
+ type Scalar = number | boolean | string;
276
+
277
+ interface Components {
278
+ readonly values: readonly Scalar[];
279
+ /** `.x`/`.y`/`.z` for vector channels; `''` for a scalar channel. */
280
+ readonly labels: readonly string[];
281
+ }
282
+
283
+ function componentsOf(kind: ChannelKind, value: ChannelValue): Components | null {
284
+ if (kind === 'vector3' || kind === 'euler' || kind === 'vector2') {
285
+ const arity = kind === 'vector2' ? 2 : 3;
286
+ if (!Array.isArray(value) || value.length !== arity) return null;
287
+ const values = value as readonly number[];
288
+ if (values.some((n) => typeof n !== 'number' || !Number.isFinite(n))) return null;
289
+ return { values, labels: arity === 2 ? ['x', 'y'] : ['x', 'y', 'z'] };
290
+ }
291
+ if (kind === 'number') {
292
+ return typeof value === 'number' && Number.isFinite(value)
293
+ ? { values: [value], labels: [''] }
294
+ : null;
295
+ }
296
+ if (kind === 'boolean') {
297
+ return typeof value === 'boolean' ? { values: [value], labels: [''] } : null;
298
+ }
299
+ // 'color' and 'string' both cross the seam as strings.
300
+ return typeof value === 'string' ? { values: [value], labels: [''] } : null;
301
+ }
302
+
303
+ /** A colour as a 24-bit int, whatever notation it arrived in — the one form two
304
+ * colour literals can be compared in. `null` when it is not a colour at all
305
+ * (a named CSS colour, `new THREE.Color(…)`): unrecognized, therefore refused. */
306
+ function colorInt(value: Scalar): number | null {
307
+ if (typeof value === 'number') return Number.isInteger(value) && value >= 0 ? value : null;
308
+ if (typeof value !== 'string') return null;
309
+ const hex = value.trim().replace(/^#/, '');
310
+ if (!/^[0-9a-fA-F]{6}$/.test(hex)) return null;
311
+ return Number.parseInt(hex, 16);
312
+ }
313
+
314
+ /** Is the literal currently in source the value currently in force? THE rule —
315
+ * see the module header. */
316
+ function sameValue(kind: ChannelKind, literal: Scalar, live: Scalar): boolean {
317
+ if (kind === 'color') {
318
+ const a = colorInt(literal);
319
+ const b = colorInt(live);
320
+ return a !== null && b !== null && a === b;
321
+ }
322
+ if (typeof literal === 'number' && typeof live === 'number') {
323
+ return Math.abs(literal - live) <= EPS;
324
+ }
325
+ return literal === live;
326
+ }
327
+
328
+ function formatNumber(n: number): string {
329
+ // Trim the float noise a gizmo drag carries without pretending to a precision
330
+ // the source never had; `-0` is normalized away because `Object.is` distinguishes
331
+ // it and no author wrote it.
332
+ const rounded = Number(n.toFixed(6));
333
+ return Object.is(rounded, -0) ? '0' : String(rounded);
334
+ }
335
+
336
+ /** Render the new value in the SAME notation the literal being replaced used —
337
+ * a game written in `0x` hex stays written in `0x` hex. */
338
+ function formatReplacement(kind: ChannelKind, value: Scalar, existingText: string): string | null {
339
+ if (kind === 'color') {
340
+ const int = colorInt(value);
341
+ if (int === null) return null;
342
+ if (/^0[xX]/.test(existingText)) return `0x${int.toString(16).padStart(6, '0')}`;
343
+ const quote = existingText[0];
344
+ if (quote === '"' || quote === "'" || quote === '`') {
345
+ return `${quote}#${int.toString(16).padStart(6, '0')}${quote}`;
346
+ }
347
+ return String(int);
348
+ }
349
+ if (kind === 'string') {
350
+ const quote = existingText[0] === '"' ? '"' : "'";
351
+ return `${quote}${String(value).replace(/\\/g, '\\\\').replace(new RegExp(quote, 'g'), `\\${quote}`)}${quote}`;
352
+ }
353
+ if (typeof value === 'boolean') return value ? 'true' : 'false';
354
+ if (typeof value === 'number') return formatNumber(value);
355
+ return null;
356
+ }
357
+
358
+ // ───────────────────────────────────────────────────────────── literal reading
359
+
360
+ interface LiteralRead {
361
+ readonly value: Scalar;
362
+ readonly start: number;
363
+ readonly end: number;
364
+ readonly text: string;
365
+ }
366
+
367
+ /**
368
+ * The literal an expression IS, or `null` when it is an expression that merely
369
+ * has a value. A leading `-` counts (authors write `-0.04`, not `0 - 0.04`);
370
+ * anything else — an identifier, a call, arithmetic — does not, and that `null`
371
+ * is what becomes "the editor never overwrites an expression".
372
+ */
373
+ function readLiteral(node: ts.Expression, sf: ts.SourceFile): LiteralRead | null {
374
+ const start = node.getStart(sf);
375
+ const end = node.getEnd();
376
+ const text = node.getText(sf);
377
+ const at = (value: Scalar): LiteralRead => ({ value, start, end, text });
378
+
379
+ if (ts.isNumericLiteral(node)) return at(Number(node.text));
380
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return at(node.text);
381
+ if (node.kind === ts.SyntaxKind.TrueKeyword) return at(true);
382
+ if (node.kind === ts.SyntaxKind.FalseKeyword) return at(false);
383
+ if (ts.isPrefixUnaryExpression(node) && ts.isNumericLiteral(node.operand)) {
384
+ if (node.operator === ts.SyntaxKind.MinusToken) return at(-Number(node.operand.text));
385
+ if (node.operator === ts.SyntaxKind.PlusToken) return at(Number(node.operand.text));
386
+ }
387
+ return null;
388
+ }
389
+
390
+ // ─────────────────────────────────────────────────────────── source navigation
391
+
392
+ /** A direct constructor or factory initializer stamped by the serve transform. */
393
+ type ConstructionExpression = ts.NewExpression | ts.CallExpression;
394
+
395
+ /** The construction expression this anchor points at, or `null` when the file
396
+ * moved under us. */
397
+ function constructionExpressionAt(
398
+ sf: ts.SourceFile,
399
+ offset: number,
400
+ ): ConstructionExpression | null {
401
+ let found: ConstructionExpression | null = null;
402
+ const visit = (node: ts.Node): void => {
403
+ if (found) return;
404
+ if (node.getEnd() <= offset || node.getStart(sf) > offset) return;
405
+ if ((ts.isNewExpression(node) || ts.isCallExpression(node)) && node.getStart(sf) === offset) {
406
+ found = node;
407
+ return;
408
+ }
409
+ ts.forEachChild(node, visit);
410
+ };
411
+ visit(sf);
412
+ return found;
413
+ }
414
+
415
+ /**
416
+ * The name the constructed object is bound to, as it must be SPELLED to address
417
+ * it again (`sun`, `this.camera`), or `null` when the construction is never
418
+ * bound at all (`scene.add(new THREE.AmbientLight(…))`) — in which case only
419
+ * the constructor's own arguments can hold this property.
420
+ */
421
+ function receiverOf(node: ConstructionExpression, sf: ts.SourceFile): string | null {
422
+ let current: ts.Node = node;
423
+ // Step out through parentheses/`as`/`!` so `const a = (new Foo())` still binds.
424
+ while (
425
+ current.parent &&
426
+ (ts.isParenthesizedExpression(current.parent) ||
427
+ ts.isAsExpression(current.parent) ||
428
+ ts.isNonNullExpression(current.parent))
429
+ ) {
430
+ current = current.parent;
431
+ }
432
+ const parent = current.parent;
433
+ if (!parent) return null;
434
+ if (ts.isVariableDeclaration(parent) && parent.initializer === current) {
435
+ return ts.isIdentifier(parent.name) ? parent.name.text : null;
436
+ }
437
+ if (ts.isPropertyDeclaration(parent) && parent.initializer === current) {
438
+ // A class field: `camera = new THREE.OrthographicCamera(…)` is addressed as
439
+ // `this.camera` by every statement that follows it.
440
+ return ts.isIdentifier(parent.name) || ts.isStringLiteral(parent.name)
441
+ ? `this.${parent.name.text}`
442
+ : null;
443
+ }
444
+ if (
445
+ ts.isBinaryExpression(parent) &&
446
+ parent.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
447
+ parent.right === current
448
+ ) {
449
+ const left = parent.left;
450
+ if (ts.isIdentifier(left) || ts.isPropertyAccessExpression(left)) return normalized(left, sf);
451
+ }
452
+ return null;
453
+ }
454
+
455
+ /** Source text with insignificant whitespace squeezed out, so `this . camera`
456
+ * and `this.camera` are the same receiver. */
457
+ function normalized(node: ts.Node, sf: ts.SourceFile): string {
458
+ return node.getText(sf).replace(/\s+/g, '');
459
+ }
460
+
461
+ type BodyNode = ts.Block | ts.SourceFile | ts.ModuleBlock;
462
+
463
+ /** The function body (or module top level) the creation statement lives in —
464
+ * the scope the spec bounds the literal search to. */
465
+ function enclosingBody(node: ts.Node): BodyNode | null {
466
+ let current: ts.Node | undefined = node;
467
+ while (current) {
468
+ if (ts.isSourceFile(current)) return current;
469
+ if (
470
+ ts.isFunctionDeclaration(current) ||
471
+ ts.isFunctionExpression(current) ||
472
+ ts.isArrowFunction(current) ||
473
+ ts.isMethodDeclaration(current) ||
474
+ ts.isConstructorDeclaration(current) ||
475
+ ts.isGetAccessorDeclaration(current) ||
476
+ ts.isSetAccessorDeclaration(current)
477
+ ) {
478
+ const body = current.body;
479
+ return body && ts.isBlock(body) ? body : null;
480
+ }
481
+ // A class field initializer's scope is the constructor's, which we cannot
482
+ // see from here; the class body is the honest boundary.
483
+ if (ts.isClassDeclaration(current) || ts.isClassExpression(current)) return null;
484
+ current = current.parent;
485
+ }
486
+ return null;
487
+ }
488
+
489
+ /**
490
+ * True when `node`'s own statement sits DIRECTLY in `body` — not nested inside
491
+ * a loop, a branch, or a callback, where source order says nothing about which
492
+ * write is in force.
493
+ *
494
+ * The check is on the NEAREST enclosing statement, not on any ancestor that
495
+ * happens to be one. A first cut climbed until an ancestor's parent was `body`
496
+ * and accepted it if that ancestor was a statement — which is true of the `for`
497
+ * a write is buried inside, so a per-iteration write read as top-level. That
498
+ * bug is what the `for`-loop case in `creation-site-edit.test.ts` pins.
499
+ */
500
+ function isTopLevelIn(node: ts.Node, body: BodyNode): boolean {
501
+ let current: ts.Node | undefined = node;
502
+ while (current && !ts.isStatement(current)) current = current.parent;
503
+ return current !== undefined && current.parent === body;
504
+ }
505
+
506
+ /** A human name for the construct a nested write is buried in, for the refusal. */
507
+ function nestingKindOf(node: ts.Node, body: BodyNode): string {
508
+ let current: ts.Node | undefined = node.parent;
509
+ while (current && current !== body) {
510
+ if (
511
+ ts.isForStatement(current) ||
512
+ ts.isForOfStatement(current) ||
513
+ ts.isForInStatement(current)
514
+ ) {
515
+ return 'loop';
516
+ }
517
+ if (ts.isWhileStatement(current) || ts.isDoStatement(current)) return 'loop';
518
+ if (ts.isIfStatement(current)) return 'conditional';
519
+ if (ts.isTryStatement(current) || ts.isCatchClause(current)) return 'try block';
520
+ if (
521
+ ts.isArrowFunction(current) ||
522
+ ts.isFunctionExpression(current) ||
523
+ ts.isFunctionDeclaration(current)
524
+ ) {
525
+ return 'nested function';
526
+ }
527
+ current = current.parent;
528
+ }
529
+ return 'nested block';
530
+ }
531
+
532
+ // ───────────────────────────────────────────────────────────── home collection
533
+
534
+ /** One literal in source that holds one component of the edited channel. */
535
+ interface Home {
536
+ readonly component: number;
537
+ readonly literal: LiteralRead;
538
+ readonly line: number;
539
+ /**
540
+ * The span of the whole OWN statement holding this home, present exactly
541
+ * when the home is a top-level `receiver.member[.axis] = literal;`
542
+ * expression statement — the one shape the insertion arm writes, and
543
+ * therefore the one shape {@link planCreationSiteRemoval} may delete to
544
+ * restore byte-absence. Constructor literals, `.set(…)` arguments and alias
545
+ * writes carry no statement: dropping those would rewrite the game's own
546
+ * code, not revert an authored override.
547
+ */
548
+ readonly statement?: { readonly start: number; readonly end: number };
549
+ }
550
+
551
+ /**
552
+ * A write to the channel we found but cannot rewrite — kept so the refusal can
553
+ * name it instead of silently rewriting an earlier, dead literal.
554
+ *
555
+ * `component` is what keeps this from over-refusing. SimCity's
556
+ * `grid.position.set(city.size / 2 - 0.5, -0.04, city.size / 2 - 0.5)` blocks
557
+ * `x` and `z` and holds a perfectly good literal for `y`; a blocker without a
558
+ * component would make the x/z expressions veto a y-only drag, which is the
559
+ * opposite of property-by-property honesty. `null` means the blocker is about
560
+ * the whole channel (an ambiguous constructor, a whole-channel assignment, a
561
+ * write in a nested scope) and does veto every component.
562
+ */
563
+ interface Blocker {
564
+ readonly reason: string;
565
+ readonly offset: number;
566
+ readonly component: number | null;
567
+ }
568
+
569
+ /** Does this blocker stand in the way of `component`? */
570
+ function blocks(blocker: Blocker, component: number): boolean {
571
+ return blocker.component === null || blocker.component === component;
572
+ }
573
+
574
+ interface Scan {
575
+ readonly homes: readonly Home[];
576
+ readonly blockers: readonly Blocker[];
577
+ }
578
+
579
+ function lineOf(sf: ts.SourceFile, pos: number): number {
580
+ return ts.getLineAndCharacterOfPosition(sf, pos).line + 1;
581
+ }
582
+
583
+ /**
584
+ * Every literal in the constructor's arguments that could hold this channel —
585
+ * which means every NAME-ADDRESSED one, and nothing else: a key in an
586
+ * object-literal argument matching the member
587
+ * (`new THREE.MeshBasicMaterial({ opacity: 0.2 })`).
588
+ *
589
+ * NO POSITIONAL MATCHING. A bare argument is never a home, however well its
590
+ * literal happens to agree with the value in force.
591
+ *
592
+ * Matching positionally — "the one argument whose literal equals the live
593
+ * value", on the theory that agreement is evidence — corrupts source. Three
594
+ * cases driven through this planner:
595
+ *
596
+ * const player = new Player(scene, true); // unchecking Visible wrote `false`
597
+ * const lamp = new Lamp(1); // an Intensity drag wrote the type code
598
+ * const npc = new NPC('goblin'); // a rename overwrote the model key
599
+ *
600
+ * In each the matched argument means something else entirely and merely holds a
601
+ * value equal to a three.js DEFAULT the source never set. The two guards this
602
+ * module leans on are both blind to it: the ambiguity check only fires on TWO
603
+ * matches, and the baseline-equality backstop cannot fire because THE
604
+ * COINCIDENCE *IS* BASELINE EQUALITY. Nor is the exposure exotic — the evidence
605
+ * a value carries is its entropy, and the values in play are `true`, `false`,
606
+ * `0` and `1`; `visible` alone puts every object with one boolean argument at
607
+ * risk, and it is in the inspector for every object.
608
+ *
609
+ * So the rule is the doctrine's: REFUSE RATHER THAN GUESS. For
610
+ * `new THREE.DirectionalLight(0xffffff, 2)` the honest answer to an intensity
611
+ * drag is "nothing at game.js:105 names `intensity`" — a live-only edit with a
612
+ * named reason, which is precisely the presentation the architecture asks for.
613
+ * Matching a slot by value is fabricating signature knowledge with extra steps:
614
+ * sourced from a coincidence instead of from a table, but fabricated either way.
615
+ */
616
+ function scanConstructor(
617
+ node: ConstructionExpression,
618
+ sf: ts.SourceFile,
619
+ channel: PropertyChannel,
620
+ baseline: Components,
621
+ ): Scan {
622
+ const args = node.arguments ? [...node.arguments] : [];
623
+ const homes: Home[] = [];
624
+ const blockers: Blocker[] = [];
625
+
626
+ for (const arg of args) {
627
+ if (!ts.isObjectLiteralExpression(arg)) continue;
628
+ for (const prop of arg.properties) {
629
+ if (!ts.isPropertyAssignment(prop)) continue;
630
+ const key = ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name) ? prop.name.text : '';
631
+ if (key !== channel.member) continue;
632
+ const init = prop.initializer;
633
+ if (ts.isArrayLiteralExpression(init) && baseline.values.length === init.elements.length) {
634
+ init.elements.forEach((element, index) => {
635
+ const literal = readLiteral(element, sf);
636
+ if (literal) homes.push({ component: index, literal, line: lineOf(sf, literal.start) });
637
+ else {
638
+ blockers.push({
639
+ reason: expressionRefusal(channel, baseline.labels[index]!, element, sf),
640
+ offset: element.getStart(sf),
641
+ component: index,
642
+ });
643
+ }
644
+ });
645
+ continue;
646
+ }
647
+ const literal = readLiteral(init, sf);
648
+ if (literal && baseline.values.length === 1) {
649
+ homes.push({ component: 0, literal, line: lineOf(sf, literal.start) });
650
+ } else {
651
+ blockers.push({
652
+ reason: expressionRefusal(channel, baseline.labels[0]!, init, sf),
653
+ offset: init.getStart(sf),
654
+ component: baseline.values.length === 1 ? 0 : null,
655
+ });
656
+ }
657
+ }
658
+ }
659
+ // Nothing else. A BARE POSITIONAL ARGUMENT IS NEVER A HOME — see the
660
+ // "no positional matching" note above `scanConstructor`. When the constructor
661
+ // names nothing, this scan is empty and the caller's honest answer is
662
+ // "nothing at <file>:<line> names <property>".
663
+ return { homes, blockers };
664
+ }
665
+
666
+ function expressionRefusal(
667
+ channel: PropertyChannel,
668
+ label: string,
669
+ node: ts.Node,
670
+ sf: ts.SourceFile,
671
+ ): string {
672
+ const path = label ? `${channel.member}.${label}` : channel.member;
673
+ const text = node.getText(sf).replace(/\s+/g, ' ').trim();
674
+ return (
675
+ `${path} is set from an expression at line ${lineOf(sf, node.getStart(sf))} ` +
676
+ `(\`${text}\`), not a literal — the editor never overwrites an expression`
677
+ );
678
+ }
679
+
680
+ /**
681
+ * Every write to `<receiver>.<member>` in the creation statement's enclosing
682
+ * function.
683
+ *
684
+ * Recognized as REWRITABLE: `R.m.set(a, b, c)` with the right arity, a per-axis
685
+ * assignment `R.m.x = a`, and a whole-channel assignment `R.m = a`. Recognized
686
+ * as a BLOCKER: any of those whose value is an expression, a compound assignment
687
+ * (`+=`), and any write that is not at the enclosing function's top statement
688
+ * level.
689
+ *
690
+ * Deliberately NOT modelled: `R.m.copy(v)`, `R.m.applyMatrix4(…)`, and every
691
+ * other mutating three.js method. Enumerating them would be the signature table
692
+ * this file exists to avoid, and it is unnecessary: a write we did not see moves
693
+ * the live value away from the literal we would have rewritten, and the
694
+ * baseline-equality check then refuses. Missing a mutator costs a refusal, never
695
+ * a corruption.
696
+ */
697
+ function scanReceiverWrites(
698
+ body: BodyNode,
699
+ sf: ts.SourceFile,
700
+ receiver: string,
701
+ channel: PropertyChannel,
702
+ baseline: Components,
703
+ afterOffset: number,
704
+ ): Scan {
705
+ const homes: Home[] = [];
706
+ const blockers: Blocker[] = [];
707
+ const channelPath = `${receiver}.${channel.member}`;
708
+
709
+ const note = (node: ts.Node, reason: string, component: number | null): void => {
710
+ blockers.push({ reason, offset: node.getStart(sf), component });
711
+ };
712
+
713
+ const visit = (node: ts.Node): void => {
714
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
715
+ const target = node.expression;
716
+ if (target.name.text === 'set' && normalized(target.expression, sf) === channelPath) {
717
+ if (node.getStart(sf) < afterOffset) return;
718
+ if (!isTopLevelIn(node, body)) {
719
+ note(node, nestedRefusal(channelPath, sf, node, body), null);
720
+ return;
721
+ }
722
+ if (node.arguments.length !== baseline.values.length) {
723
+ note(
724
+ node,
725
+ `${channelPath}.set(…) at line ${lineOf(sf, node.getStart(sf))} passes ` +
726
+ `${node.arguments.length} arguments for a ${baseline.values.length}-component ` +
727
+ "value — the editor will not change a call's arity",
728
+ null,
729
+ );
730
+ return;
731
+ }
732
+ node.arguments.forEach((arg, index) => {
733
+ const literal = readLiteral(arg, sf);
734
+ if (literal) homes.push({ component: index, literal, line: lineOf(sf, literal.start) });
735
+ else note(arg, expressionRefusal(channel, baseline.labels[index]!, arg, sf), index);
736
+ });
737
+ return;
738
+ }
739
+ }
740
+
741
+ if (
742
+ ts.isBinaryExpression(node) &&
743
+ ts.isPropertyAccessExpression(node.left) &&
744
+ isAssignmentOperator(node.operatorToken.kind)
745
+ ) {
746
+ const left = normalized(node.left, sf);
747
+ const isWhole = left === channelPath;
748
+ const axis = baseline.labels.indexOf(node.left.name.text);
749
+ const isAxis =
750
+ axis >= 0 &&
751
+ baseline.labels[0] !== '' &&
752
+ normalized(node.left.expression, sf) === channelPath;
753
+ if ((isWhole || isAxis) && node.getStart(sf) >= afterOffset) {
754
+ const component = isWhole ? 0 : axis;
755
+ if (node.operatorToken.kind !== ts.SyntaxKind.EqualsToken) {
756
+ note(
757
+ node,
758
+ `${left} is updated in place at line ${lineOf(sf, node.getStart(sf))} ` +
759
+ `(\`${node.getText(sf).replace(/\s+/g, ' ').trim()}\`) — the editor never rewrites a ` +
760
+ 'compound assignment',
761
+ isWhole ? null : component,
762
+ );
763
+ return;
764
+ }
765
+ if (isWhole && baseline.values.length !== 1) {
766
+ note(
767
+ node,
768
+ `${left} is assigned whole at line ${lineOf(sf, node.getStart(sf))} ` +
769
+ `(\`${node.right.getText(sf).replace(/\s+/g, ' ').trim()}\`) — a ` +
770
+ `${baseline.values.length}-component value has no literal to rewrite there`,
771
+ null,
772
+ );
773
+ return;
774
+ }
775
+ if (!isTopLevelIn(node, body)) {
776
+ note(node, nestedRefusal(left, sf, node, body), isWhole ? null : component);
777
+ return;
778
+ }
779
+ const literal = readLiteral(node.right, sf);
780
+ if (literal) {
781
+ const statement =
782
+ ts.isExpressionStatement(node.parent) && node.parent.expression === node
783
+ ? { start: node.parent.getStart(sf), end: node.parent.getEnd() }
784
+ : undefined;
785
+ homes.push({
786
+ component,
787
+ literal,
788
+ line: lineOf(sf, literal.start),
789
+ ...(statement ? { statement } : {}),
790
+ });
791
+ } else {
792
+ note(
793
+ node.right,
794
+ expressionRefusal(channel, baseline.labels[component]!, node.right, sf),
795
+ component,
796
+ );
797
+ }
798
+ return;
799
+ }
800
+ }
801
+
802
+ ts.forEachChild(node, visit);
803
+ };
804
+ ts.forEachChild(body, visit);
805
+ return { homes, blockers };
806
+ }
807
+
808
+ /**
809
+ * Is this binary operator a WRITE (`=`, `+=`, `??=`, …) rather than a read?
810
+ *
811
+ * `scanReceiverWrites` looks for `<receiver>.<member> <op> <value>`, and without
812
+ * this guard it read ANY binary expression whose left side is that member path
813
+ * as a write — so `const bx = bird.x - 14` was reported as "bird.x is updated in
814
+ * place … the editor never rewrites a compound assignment", refusing a
815
+ * perfectly writable edit with a sentence that is not true of the line it names.
816
+ *
817
+ * It only became reachable with `componentMembers`: with the three channels the
818
+ * left side is `sun.position`, and no real game does arithmetic on a `Vector3`
819
+ * member path, so the bug sat unfired. Pixi's `bird.x` is a NUMBER, and reading
820
+ * it in arithmetic is ordinary in every frame of every game. Found live on the
821
+ * flappy ingest fixture, not by a test.
822
+ */
823
+ function isAssignmentOperator(kind: ts.SyntaxKind): boolean {
824
+ return kind >= ts.SyntaxKind.FirstAssignment && kind <= ts.SyntaxKind.LastAssignment;
825
+ }
826
+
827
+ function nestedRefusal(path: string, sf: ts.SourceFile, node: ts.Node, body: BodyNode): string {
828
+ return (
829
+ `${path} is also written at line ${lineOf(sf, node.getStart(sf))} inside a ` +
830
+ `${nestingKindOf(node, body)} — the editor cannot know which write is in force`
831
+ );
832
+ }
833
+
834
+ /**
835
+ * The channel's ALIAS spellings — each `componentMembers` entry scanned as if it
836
+ * were its own one-component channel, with the resulting homes and blockers
837
+ * relabelled onto the component they actually hold.
838
+ *
839
+ * Nothing new is decided here. It re-runs {@link scanConstructor} and
840
+ * {@link scanReceiverWrites} verbatim over a synthetic scalar channel, so
841
+ * `hud.x = 16` is accepted, refused and worded by exactly the rules that govern
842
+ * `hud.intensity = 16` — including the nested-scope, compound-assignment and
843
+ * expression refusals. That reuse is the point: an alias must not become a
844
+ * second, laxer path to the same bytes.
845
+ */
846
+ function scanComponentAliases(
847
+ construction: ConstructionExpression,
848
+ sf: ts.SourceFile,
849
+ channel: PropertyChannel,
850
+ baseline: Components,
851
+ receiver: string | null,
852
+ body: BodyNode | null,
853
+ ): Scan {
854
+ const homes: Home[] = [];
855
+ const blockers: Blocker[] = [];
856
+ const aliases = channel.componentMembers;
857
+ if (!aliases) return { homes, blockers };
858
+ aliases.forEach((member, component) => {
859
+ const value = baseline.values[component];
860
+ if (value === undefined) return;
861
+ const scalar: PropertyChannel = { owner: channel.owner, member, kind: 'number' };
862
+ const one: Components = { values: [value], labels: [''] };
863
+ const scans: Scan[] = [scanConstructor(construction, sf, scalar, one)];
864
+ if (receiver && body) {
865
+ scans.push(scanReceiverWrites(body, sf, receiver, scalar, one, construction.getStart(sf)));
866
+ }
867
+ for (const scan of scans) {
868
+ for (const home of scan.homes) homes.push({ ...home, component });
869
+ for (const blocker of scan.blockers) blockers.push({ ...blocker, component });
870
+ }
871
+ });
872
+ return { homes, blockers };
873
+ }
874
+
875
+ // ─────────────────────────────────────────────────────────────────── the plan
876
+
877
+ /**
878
+ * WHICH LITERAL HOLDS EACH COMPONENT of one channel at one creation site — the
879
+ * whole of "read the source" with none of "decide the write".
880
+ *
881
+ * Extracted so the READ surface ({@link readCreationSiteLiteral}, which shows an
882
+ * instance's overrides against the component default the site names) and the
883
+ * WRITE decision ({@link planCreationSiteEdit}) cannot drift into two scanning
884
+ * rules. Every judgement about what a literal IS stays here; every judgement
885
+ * about whether replacing it is TRUE stays in the caller.
886
+ */
887
+ interface SiteScan {
888
+ readonly sf: ts.SourceFile;
889
+ /** Component index → the literal in force for it (LAST WRITER WINS). */
890
+ readonly chosen: ReadonlyMap<number, Home>;
891
+ /** EVERY home found, chosen or superseded — the removal planner needs the
892
+ * losers too: deleting the last writer while an earlier author stands
893
+ * would fall back to that author's value, not to absence. */
894
+ readonly homes: readonly Home[];
895
+ readonly blockers: readonly Blocker[];
896
+ /**
897
+ * Where a per-axis assignment could be INSERTED when a component has no
898
+ * home and no blocker: right after the statement that binds the
899
+ * construction to `receiver`. `null` when the construction is not a
900
+ * body-level `const x = new …` / `x = new …` — a class-field initializer
901
+ * or an expression-position construction has no honest statement slot.
902
+ */
903
+ readonly insertAt: {
904
+ readonly offset: number;
905
+ readonly indent: string;
906
+ readonly receiver: string;
907
+ } | null;
908
+ }
909
+
910
+ function scanSite(
911
+ source: string,
912
+ site: CreationSite,
913
+ channel: PropertyChannel,
914
+ baseline: Components,
915
+ ): SiteScan | { readonly refusal: string } {
916
+ const sf = ts.createSourceFile(
917
+ site.file,
918
+ source,
919
+ ts.ScriptTarget.Latest,
920
+ /* setParentNodes */ true,
921
+ scriptKindFor(site.file),
922
+ );
923
+ const offset = lineColToOffset(source, site.line, site.col);
924
+ const construction = constructionExpressionAt(sf, offset);
925
+ if (!construction) {
926
+ return {
927
+ refusal:
928
+ `${site.file}:${site.line} no longer holds a constructor or factory call — ` +
929
+ 'the file changed since the game was loaded',
930
+ };
931
+ }
932
+
933
+ const scans: Scan[] = [scanConstructor(construction, sf, channel, baseline)];
934
+ const receiver = receiverOf(construction, sf);
935
+ const body = receiver ? enclosingBody(construction) : null;
936
+ if (receiver && body) {
937
+ scans.push(
938
+ scanReceiverWrites(body, sf, receiver, channel, baseline, construction.getStart(sf)),
939
+ );
940
+ }
941
+ scans.push(scanComponentAliases(construction, sf, channel, baseline, receiver, body));
942
+
943
+ const homes = scans.flatMap((scan) => scan.homes);
944
+ const blockers = scans.flatMap((scan) => scan.blockers);
945
+
946
+ // LAST WRITER WINS, by source position: everything we look at is either inside
947
+ // the `new` (which runs first) or a top-level statement of the same function
948
+ // after it, so textual order IS execution order here — which is exactly why
949
+ // anything nested became a blocker above rather than a candidate.
950
+ const chosen = new Map<number, Home>();
951
+ for (const home of [...homes].sort((a, b) => a.literal.start - b.literal.start)) {
952
+ chosen.set(home.component, home);
953
+ }
954
+ return {
955
+ sf,
956
+ chosen,
957
+ homes,
958
+ blockers,
959
+ insertAt: insertionAnchor(construction, sf, source, receiver),
960
+ };
961
+ }
962
+
963
+ /** The insertion slot for {@link SiteScan.insertAt} — see its doc. */
964
+ function insertionAnchor(
965
+ construction: ConstructionExpression,
966
+ sf: ts.SourceFile,
967
+ source: string,
968
+ receiver: string | null,
969
+ ): SiteScan['insertAt'] {
970
+ if (!receiver) return null;
971
+ // Walk up to the statement holding the construction — and the walk may not
972
+ // CROSS a class, function, or property boundary on the way, because the
973
+ // first `isStatement` hit above such a boundary is some ENCLOSING
974
+ // declaration, not the construction's own statement. The shipped corruption
975
+ // this refuses (measured on bubbo-bubbo): a class-property initializer
976
+ // (`private readonly _hitContainer = new Container()`) walked up to the
977
+ // CLASS declaration — a statement — and the insertion landed after the
978
+ // class's closing brace as `this._hitContainer.position.y = 1.5;`, where
979
+ // `this` is dead: the module then throws at load and the whole game stops
980
+ // mounting. Only the construction's OWN statement — a variable statement or
981
+ // an assignment expression statement — puts the inserted assignment in the
982
+ // scope where the receiver is genuinely in force.
983
+ let node: ts.Node = construction;
984
+ while (node.parent && !ts.isStatement(node)) {
985
+ if (ts.isClassLike(node) || ts.isFunctionLike(node) || ts.isPropertyDeclaration(node)) {
986
+ return null;
987
+ }
988
+ node = node.parent;
989
+ }
990
+ if (!ts.isVariableStatement(node) && !ts.isExpressionStatement(node)) return null;
991
+ const holder = node.parent;
992
+ if (!holder || !(ts.isBlock(holder) || ts.isSourceFile(holder))) return null;
993
+ const statementStart = node.getStart(sf);
994
+ const lineStart = source.lastIndexOf('\n', statementStart - 1) + 1;
995
+ const indentText = source.slice(lineStart, statementStart);
996
+ if (!/^[ \t]*$/.test(indentText)) return null; // shares a line with other code
997
+ return { offset: node.getEnd(), indent: indentText, receiver };
998
+ }
999
+
1000
+ /** The refusal for a component with no rewritable literal, in the planner's own
1001
+ * words: the blocker that stands in its way, or the plain absence. */
1002
+ function homeRefusal(
1003
+ site: CreationSite,
1004
+ channel: PropertyChannel,
1005
+ label: string,
1006
+ blockers: readonly Blocker[],
1007
+ ): string {
1008
+ const path = label ? `${channel.member}.${label}` : channel.member;
1009
+ const blocker = [...blockers].sort((a, b) => b.offset - a.offset)[0];
1010
+ return (
1011
+ blocker?.reason ??
1012
+ `nothing at ${site.file}:${site.line} names ${path} — the value has no literal to rewrite`
1013
+ );
1014
+ }
1015
+
1016
+ /** The refusal when a component with no home could only be written by INSERTION
1017
+ * but the construction is not bound to a name at statement level. ONE spelling,
1018
+ * shared by {@link planCreationSiteEdit} and {@link readCreationSiteLiteral},
1019
+ * because the read's `writable` is defined as "would the planner accept?" and a
1020
+ * paraphrase here would let the two answers drift apart. */
1021
+ function insertionRefusal(site: CreationSite, channel: PropertyChannel): string {
1022
+ return (
1023
+ `nothing at ${site.file}:${site.line} names ${channel.member}, and the ` +
1024
+ 'constructed object is not bound to a name at statement level — the editor has no ' +
1025
+ 'honest place to write the value into this source'
1026
+ );
1027
+ }
1028
+
1029
+ /**
1030
+ * Plan one property edit against the game's own source.
1031
+ *
1032
+ * `source` is the file's bytes as they sit on disk — the caller reads them; this
1033
+ * function does no I/O so that both the dev server and a test can drive the
1034
+ * same decision.
1035
+ */
1036
+ export function planCreationSiteEdit(
1037
+ source: string,
1038
+ request: CreationSiteEditRequest,
1039
+ ): CreationSiteEditPlan {
1040
+ if (request.instances !== 1 && request.writeScope !== 'creation-site') {
1041
+ return refuse(source, multiplicityRefusal(request.instances));
1042
+ }
1043
+ const channel = channelFor(request.property, request.surface);
1044
+ if (!channel) {
1045
+ return refuse(source, `the editor has no source expression for "${request.property}"`);
1046
+ }
1047
+ const baseline = componentsOf(channel.kind, request.baseline);
1048
+ const next = componentsOf(channel.kind, request.next);
1049
+ if (!baseline || !next) {
1050
+ return refuse(source, `"${request.property}" did not arrive as a ${channel.kind} value`);
1051
+ }
1052
+
1053
+ const changedComponents = baseline.values
1054
+ .map((value, index) => (sameValue(channel.kind, value, next.values[index]!) ? -1 : index))
1055
+ .filter((index) => index >= 0);
1056
+ if (changedComponents.length === 0) {
1057
+ return { changed: false, prevSource: source, newSource: source, reason: 'nothing changed' };
1058
+ }
1059
+
1060
+ const scan = scanSite(source, request.site, channel, baseline);
1061
+ if ('refusal' in scan) return refuse(source, scan.refusal);
1062
+ const { sf, chosen, blockers, insertAt } = scan;
1063
+
1064
+ const edits: Array<{ start: number; end: number; text: string }> = [];
1065
+ const insertions: Array<{ path: string; value: Scalar }> = [];
1066
+ for (const component of changedComponents) {
1067
+ const label = baseline.labels[component]!;
1068
+ const path = label ? `${channel.member}.${label}` : channel.member;
1069
+ const relevant = blockers.filter((blocker) => blocks(blocker, component));
1070
+ const home = chosen.get(component);
1071
+ if (!home) {
1072
+ // INSERTION — the one case where a write can be honest without a
1073
+ // literal to rewrite: the site names NOTHING for this component (no
1074
+ // home, and no blocker either — a blocker would mean something else
1075
+ // already computes or writes it, and inserting a second writer would
1076
+ // fight it), and the construction binds a named receiver at statement
1077
+ // level. Then a per-axis named assignment right after the construction
1078
+ // statement IS the smallest source that produces the dragged value:
1079
+ // it names the property (rule 1 by construction — we write the name),
1080
+ // it needs no library knowledge (the channel path demonstrably exists
1081
+ // on the live object the baseline was read from), and it is exactly
1082
+ // the shape `scanReceiverWrites` already recognizes — so the NEXT drag
1083
+ // rewrites this literal through the ordinary path instead of
1084
+ // inserting again. An options-bag key is deliberately NOT the target:
1085
+ // whether a constructor reads a given key is the signature table this
1086
+ // module refuses to own.
1087
+ if (relevant.length === 0) {
1088
+ insertions.push({ path, value: next.values[component]! });
1089
+ continue;
1090
+ }
1091
+ return refuse(source, homeRefusal(request.site, channel, label, relevant));
1092
+ }
1093
+ // A blocker for THIS component that runs after the home we picked means
1094
+ // something we cannot rewrite has the last word; rewriting the earlier
1095
+ // literal would be a dead edit that silently does nothing at runtime.
1096
+ const later = relevant.find((blocker) => blocker.offset > home.literal.start);
1097
+ if (later) return refuse(source, later.reason);
1098
+
1099
+ // THE BACKSTOP (module header, rule 2).
1100
+ if (!sameValue(channel.kind, home.literal.value, baseline.values[component]!)) {
1101
+ return refuse(
1102
+ source,
1103
+ `the literal at ${request.site.file}:${home.line} is \`${home.literal.text}\`, but ` +
1104
+ `${path} is ${describe(baseline.values[component]!)} in the running game — ` +
1105
+ 'the value is computed at runtime, so writing that literal would not produce it',
1106
+ );
1107
+ }
1108
+ const replacement = formatReplacement(channel.kind, next.values[component]!, home.literal.text);
1109
+ if (replacement === null) {
1110
+ return refuse(source, `the editor cannot express this ${channel.kind} value in source`);
1111
+ }
1112
+ edits.push({ start: home.literal.start, end: home.literal.end, text: replacement });
1113
+ }
1114
+
1115
+ if (insertions.length > 0) {
1116
+ if (!insertAt) {
1117
+ return refuse(source, insertionRefusal(request.site, channel));
1118
+ }
1119
+ const lines = insertions
1120
+ .map(({ path, value }) => {
1121
+ const text =
1122
+ typeof value === 'number'
1123
+ ? formatNumber(value)
1124
+ : typeof value === 'boolean'
1125
+ ? String(value)
1126
+ : typeof value === 'string'
1127
+ ? `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`
1128
+ : null;
1129
+ return text === null ? null : `\n${insertAt.indent}${insertAt.receiver}.${path} = ${text};`;
1130
+ })
1131
+ .filter((line): line is string => line !== null);
1132
+ if (lines.length !== insertions.length) {
1133
+ return refuse(source, `the editor cannot express this ${channel.kind} value in source`);
1134
+ }
1135
+ edits.push({ start: insertAt.offset, end: insertAt.offset, text: lines.join('') });
1136
+ }
1137
+
1138
+ edits.sort((a, b) => a.start - b.start);
1139
+ let out = '';
1140
+ let cursor = 0;
1141
+ for (const edit of edits) {
1142
+ out += source.slice(cursor, edit.start) + edit.text;
1143
+ cursor = edit.end;
1144
+ }
1145
+ out += source.slice(cursor);
1146
+ if (out === source) {
1147
+ return { changed: false, prevSource: source, newSource: source, reason: 'nothing changed' };
1148
+ }
1149
+ return {
1150
+ changed: true,
1151
+ prevSource: source,
1152
+ newSource: out,
1153
+ wrote: `${request.property} at ${request.site.file}:${edits.length === 1 ? lineOf(sf, edits[0]!.start) : request.site.line}`,
1154
+ };
1155
+ }
1156
+
1157
+ /** A removal is a write request minus BOTH values — absence is not a value,
1158
+ * and a shape that carried one would be a value write wearing another name
1159
+ * (the engine's `SourceRemoveRequest` states the same rule). The channel's
1160
+ * arity and labels are static facts of its kind, so no live reading is
1161
+ * needed to scan for the authored statements. */
1162
+ export interface CreationSiteRemovalRequest {
1163
+ readonly site: CreationSite;
1164
+ readonly instances: number;
1165
+ readonly writeScope: CreationSiteWriteScope;
1166
+ readonly property: string;
1167
+ readonly surface: CreationSiteSurface;
1168
+ }
1169
+
1170
+ /** The channel's shape with no live value behind it — labels and arity only,
1171
+ * which is all scanning reads (the write path's equality backstop is the one
1172
+ * consumer of real component VALUES, and a removal never compares). */
1173
+ function componentsShapeOf(kind: ChannelKind): Components {
1174
+ if (kind === 'vector3' || kind === 'euler') {
1175
+ return { values: [0, 0, 0], labels: ['x', 'y', 'z'] };
1176
+ }
1177
+ if (kind === 'vector2') return { values: [0, 0], labels: ['x', 'y'] };
1178
+ if (kind === 'number') return { values: [0], labels: [''] };
1179
+ if (kind === 'boolean') return { values: [false], labels: [''] };
1180
+ return { values: [''], labels: [''] };
1181
+ }
1182
+
1183
+ /**
1184
+ * Plan the REMOVAL of a property's authored value from the game's own source —
1185
+ * the byte-absence door the insertion arm makes necessary: an authored
1186
+ * transform can ADD a `receiver.member.axis = value;` statement for an axis
1187
+ * the source never named, and no value write can revert an added property.
1188
+ *
1189
+ * THE ONE DELETABLE SHAPE is exactly the shape insertion writes: a whole
1190
+ * own-line, top-level `receiver.member[.axis] = <literal>;` expression
1191
+ * statement ({@link Home.statement}). Everything else refuses in its own
1192
+ * words — a constructor literal or `.set(…)` argument is the game's own code
1193
+ * (deleting it would change the construction, not drop an override), a
1194
+ * blocker's expression is something the game computes, and a statement
1195
+ * sharing its line with other code cannot be deleted without taking that code
1196
+ * with it. A component the source never names is simply already absent.
1197
+ *
1198
+ * Deletion removes the statement's line whole — the preceding newline through
1199
+ * the statement's trailing semicolon and any same-line whitespace — which is
1200
+ * the byte-exact inverse of what insertion appended.
1201
+ */
1202
+ export function planCreationSiteRemoval(
1203
+ source: string,
1204
+ request: CreationSiteRemovalRequest,
1205
+ ): CreationSiteEditPlan {
1206
+ if (request.instances !== 1 && request.writeScope !== 'creation-site') {
1207
+ return refuse(source, multiplicityRefusal(request.instances));
1208
+ }
1209
+ const channel = channelFor(request.property, request.surface);
1210
+ if (!channel) {
1211
+ return refuse(source, `the editor has no source expression for "${request.property}"`);
1212
+ }
1213
+ const baseline = componentsShapeOf(channel.kind);
1214
+ const scan = scanSite(source, request.site, channel, baseline);
1215
+ if ('refusal' in scan) return refuse(source, scan.refusal);
1216
+ const { sf, chosen, homes, blockers } = scan;
1217
+
1218
+ const statements: Array<{ start: number; end: number }> = [];
1219
+ for (let component = 0; component < baseline.values.length; component++) {
1220
+ const home = chosen.get(component);
1221
+ const relevant = blockers.filter((blocker) => blocks(blocker, component));
1222
+ if (!home) {
1223
+ // No rewritable home, but something still MENTIONS this component — an
1224
+ // expression, a compound assignment, a constructor shape the scanner
1225
+ // cannot read. That is not absence, and deleting cannot make it absence,
1226
+ // so the refusal is the blocker's own description. With no blocker
1227
+ // either, nothing names this component and there is nothing to do.
1228
+ const blocker = [...relevant].sort((a, b) => b.offset - a.offset)[0];
1229
+ if (blocker) return refuse(source, blocker.reason);
1230
+ continue;
1231
+ }
1232
+ const label = baseline.labels[component]!;
1233
+ const path = label ? `${channel.member}.${label}` : channel.member;
1234
+ if (!home.statement) {
1235
+ return refuse(
1236
+ source,
1237
+ `${path} is authored at ${request.site.file}:${home.line} inside the construction ` +
1238
+ 'itself (a constructor literal, a `.set(…)` argument, or an alias write) — that is ' +
1239
+ "the game's own code, not a removable own-line assignment, so the editor will not " +
1240
+ 'delete it',
1241
+ );
1242
+ }
1243
+ // A blocker AFTER the chosen home means something unremovable has the last
1244
+ // word; deleting the earlier statement would not produce absence.
1245
+ const later = relevant.find((blocker) => blocker.offset > home.statement!.start);
1246
+ if (later) return refuse(source, later.reason);
1247
+ // A second author for the same component: deleting the last writer would
1248
+ // fall back to the earlier author's value, not to absence.
1249
+ const earlier = homes.find((other) => other.component === component && other !== home);
1250
+ if (earlier) {
1251
+ return refuse(
1252
+ source,
1253
+ `${path} is authored more than once (line ${earlier.line} and line ${home.line}) — ` +
1254
+ `deleting the last writer would fall back to line ${earlier.line}'s value, ` +
1255
+ 'not to absence',
1256
+ );
1257
+ }
1258
+ statements.push(home.statement);
1259
+ }
1260
+
1261
+ if (statements.length === 0) {
1262
+ return {
1263
+ changed: false,
1264
+ prevSource: source,
1265
+ newSource: source,
1266
+ reason:
1267
+ `nothing at ${request.site.file}:${request.site.line} authors ${channel.member} as an ` +
1268
+ 'own-line assignment — the value is already absent from the source',
1269
+ };
1270
+ }
1271
+
1272
+ // Deduplicate (a whole-channel statement can be chosen by several
1273
+ // components) and delete, verifying each statement owns its line.
1274
+ const spans = [...new Map(statements.map((s) => [s.start, s])).values()].sort(
1275
+ (a, b) => a.start - b.start,
1276
+ );
1277
+ let out = '';
1278
+ let cursor = 0;
1279
+ for (const span of spans) {
1280
+ const lineStart = source.lastIndexOf('\n', span.start - 1) + 1;
1281
+ if (!/^[ \t]*$/.test(source.slice(lineStart, span.start))) {
1282
+ return refuse(
1283
+ source,
1284
+ `the assignment at ${request.site.file}:${lineOf(sf, span.start)} shares its line with ` +
1285
+ "other code — deleting the line would take the game's own code with it",
1286
+ );
1287
+ }
1288
+ let lineEnd = source.indexOf('\n', span.end);
1289
+ if (lineEnd === -1) lineEnd = source.length;
1290
+ if (!/^[ \t]*$/.test(source.slice(span.end, lineEnd))) {
1291
+ return refuse(
1292
+ source,
1293
+ `the assignment at ${request.site.file}:${lineOf(sf, span.start)} shares its line with ` +
1294
+ "other code — deleting the line would take the game's own code with it",
1295
+ );
1296
+ }
1297
+ // Delete the preceding newline through the line's end (the byte-exact
1298
+ // inverse of insertion's `\n<indent><stmt>;`); at the file's first line
1299
+ // delete through the trailing newline instead.
1300
+ const deleteStart = lineStart === 0 ? 0 : lineStart - 1;
1301
+ const deleteEnd = lineStart === 0 ? Math.min(lineEnd + 1, source.length) : lineEnd;
1302
+ out += source.slice(cursor, deleteStart);
1303
+ cursor = deleteEnd;
1304
+ }
1305
+ out += source.slice(cursor);
1306
+ if (out === source) {
1307
+ return { changed: false, prevSource: source, newSource: source, reason: 'nothing changed' };
1308
+ }
1309
+ return {
1310
+ changed: true,
1311
+ prevSource: source,
1312
+ newSource: out,
1313
+ wrote: `${request.property} removed at ${request.site.file}:${lineOf(sf, spans[0]!.start)}`,
1314
+ };
1315
+ }
1316
+
1317
+ function describe(value: Scalar): string {
1318
+ return typeof value === 'number' ? formatNumber(value) : String(value);
1319
+ }
1320
+
1321
+ // ───────────────────────────────────────────────── the read: what the site says
1322
+
1323
+ /**
1324
+ * ONE property, at one creation site, READ rather than written — what the
1325
+ * construction statement says the value is, so a surface can show the live
1326
+ * object's value as an OVERRIDE of it.
1327
+ *
1328
+ * `live` is the value in force. Its shape decides the channel's arity, exactly
1329
+ * as {@link CreationSiteEditRequest.baseline} does, so a caller cannot ask this
1330
+ * question in a vocabulary the write path would reject.
1331
+ */
1332
+ export interface CreationSiteLiteralRequest {
1333
+ readonly site: CreationSite;
1334
+ /** How many objects the anchor constructed — a shared site is unwritable for
1335
+ * an ordinary instance edit and writable only for the explicit
1336
+ * creation-site-default gesture that names the full count. */
1337
+ readonly instances: number;
1338
+ /** Which write the `writable` verdict is answering for. */
1339
+ readonly writeScope?: CreationSiteWriteScope;
1340
+ readonly property: string;
1341
+ readonly surface?: CreationSiteSurface;
1342
+ readonly live: ChannelValue;
1343
+ }
1344
+
1345
+ /**
1346
+ * What the site says, and whether it could be made to say something else.
1347
+ *
1348
+ * `literal` is normalized into the CALLER's own notation (a `0xff00ff` colour
1349
+ * literal comes back as `#ff00ff`, because that is the form the editor's colour
1350
+ * rows read and write), so a caller can put it straight back on the live object
1351
+ * without knowing how the game happened to spell it.
1352
+ */
1353
+ export interface CreationSiteLiteralReport {
1354
+ /** The value the construction site names, or `null` when it names none. */
1355
+ readonly literal: ChannelValue | null;
1356
+ /** The literal's own source text (per component, comma-joined) — the string a
1357
+ * surface shows as "the component default". */
1358
+ readonly text?: string;
1359
+ /** Would {@link planCreationSiteEdit} accept a write of a new value here? */
1360
+ readonly writable: boolean;
1361
+ /** Present iff `!writable` — the planner's own words, never a paraphrase. */
1362
+ readonly reason?: string;
1363
+ }
1364
+
1365
+ /** Put one literal back in the caller's own notation (see {@link
1366
+ * CreationSiteLiteralReport.literal}). */
1367
+ function normalizeLiteral(kind: ChannelKind, value: Scalar): Scalar | null {
1368
+ if (kind !== 'color') return value;
1369
+ const int = colorInt(value);
1370
+ return int === null ? null : `#${int.toString(16).padStart(6, '0')}`;
1371
+ }
1372
+
1373
+ export function readCreationSiteLiteral(
1374
+ source: string,
1375
+ request: CreationSiteLiteralRequest,
1376
+ ): CreationSiteLiteralReport {
1377
+ const channel = channelFor(request.property, request.surface);
1378
+ if (!channel) {
1379
+ return {
1380
+ literal: null,
1381
+ writable: false,
1382
+ reason: `the editor has no source expression for "${request.property}"`,
1383
+ };
1384
+ }
1385
+ const live = componentsOf(channel.kind, request.live);
1386
+ if (!live) {
1387
+ return {
1388
+ literal: null,
1389
+ writable: false,
1390
+ reason: `"${request.property}" did not arrive as a ${channel.kind} value`,
1391
+ };
1392
+ }
1393
+ const scan = scanSite(source, request.site, channel, live);
1394
+ if ('refusal' in scan) {
1395
+ return { literal: null, writable: false, reason: scan.refusal };
1396
+ }
1397
+
1398
+ const values: Scalar[] = [];
1399
+ const texts: string[] = [];
1400
+ // A component with NO literal ends the read literal-less (there is no default
1401
+ // to show) — unless the planner would refuse it outright, ending the read
1402
+ // with that refusal. One that merely cannot be REWRITTEN does neither: the
1403
+ // first such reason is kept and the values are still reported. That is the
1404
+ // difference between "we cannot show you the default" and "we can show it
1405
+ // but cannot change it".
1406
+ let refusal: string | null = null;
1407
+ let insertable = false;
1408
+ for (let component = 0; component < live.values.length; component++) {
1409
+ const relevant = scan.blockers.filter((blocker) => blocks(blocker, component));
1410
+ const home = scan.chosen.get(component);
1411
+ if (!home) {
1412
+ // MIRROR THE PLANNER's homeless arm exactly (`writable` is defined as
1413
+ // "would planCreationSiteEdit accept a write here?"): with a blocker the
1414
+ // write refuses; unblocked and unbound it refuses in the insertion arm's
1415
+ // own words; unblocked with an insertion anchor it WRITES — by inserting
1416
+ // a named assignment — so this report must say writable even though the
1417
+ // site names no default to show (`literal` stays null for the property,
1418
+ // which is the truth: an insertable value has no source default). The
1419
+ // scan continues, because a LATER component's blocker still refuses the
1420
+ // whole write and this report must say so.
1421
+ if (relevant.length > 0) {
1422
+ return {
1423
+ literal: null,
1424
+ writable: false,
1425
+ reason: homeRefusal(request.site, channel, live.labels[component]!, relevant),
1426
+ };
1427
+ }
1428
+ if (!scan.insertAt) {
1429
+ return { literal: null, writable: false, reason: insertionRefusal(request.site, channel) };
1430
+ }
1431
+ insertable = true;
1432
+ continue;
1433
+ }
1434
+ const normalized = normalizeLiteral(channel.kind, home.literal.value);
1435
+ if (normalized === null) {
1436
+ return {
1437
+ literal: null,
1438
+ writable: false,
1439
+ reason:
1440
+ `the literal at ${request.site.file}:${home.line} is \`${home.literal.text}\`, which ` +
1441
+ `the editor cannot read as a ${channel.kind}`,
1442
+ };
1443
+ }
1444
+ values.push(normalized);
1445
+ texts.push(home.literal.text);
1446
+ const later = relevant.find((blocker) => blocker.offset > home.literal.start);
1447
+ if (later) refusal ??= later.reason;
1448
+ }
1449
+ if (request.instances !== 1 && request.writeScope !== 'creation-site') {
1450
+ refusal ??= multiplicityRefusal(request.instances);
1451
+ }
1452
+
1453
+ if (insertable) {
1454
+ // At least one component would be written by insertion, so the site names
1455
+ // no complete default — `literal` is null (nothing honest to show), while
1456
+ // `writable` still answers for the whole write.
1457
+ return {
1458
+ literal: null,
1459
+ writable: refusal === null,
1460
+ ...(refusal === null ? {} : { reason: refusal }),
1461
+ };
1462
+ }
1463
+
1464
+ const literal: ChannelValue =
1465
+ live.values.length > 1 ? (values as readonly number[]) : (values[0] as Scalar);
1466
+ return {
1467
+ literal,
1468
+ text: texts.join(', '),
1469
+ writable: refusal === null,
1470
+ ...(refusal === null ? {} : { reason: refusal }),
1471
+ };
1472
+ }
1473
+
1474
+ function scriptKindFor(file: string): ts.ScriptKind {
1475
+ if (file.endsWith('.tsx')) return ts.ScriptKind.TSX;
1476
+ if (file.endsWith('.jsx')) return ts.ScriptKind.JSX;
1477
+ if (file.endsWith('.ts')) return ts.ScriptKind.TS;
1478
+ return ts.ScriptKind.JS;
1479
+ }