@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,1845 @@
1
+ /**
2
+ * OID instrumentation (C1/C2) — the visual-edit "stamp every JSX element with a
3
+ * stable source id" trick, ported to vgai using the TypeScript compiler API
4
+ * (vgai has `typescript`; it does NOT have @babel/*). Hand-authored React UI
5
+ * component source is transformed at build/dev time to carry a `data-oid` on every
6
+ * JSX element, and an OID -> {file,line,col,component,tag} index is built so the
7
+ * editor can map a clicked element back to its exact source location.
8
+ *
9
+ * Stability (C1): unlike visual-edit's line:col key (which shifts every id below an
10
+ * edit), we key by a CONTENT signature `component:tag:nthOccurrence`, so an edit on
11
+ * an unrelated line keeps existing ids stable. The `data-oid` lives ONLY in the
12
+ * transformed output, never on disk (the writer edits the original source instead).
13
+ */
14
+ import ts from 'typescript';
15
+ import {
16
+ environmentBindingsByElement,
17
+ type R3fEnvironmentBinding,
18
+ } from './r3f-environment-binding';
19
+ import { jointBindingsByElement, type R3fJointBinding } from './r3f-joint-binding';
20
+ import { lodBindingsByElement, type R3fLodBinding } from './r3f-lod-binding';
21
+ import { particleBindingsByElement, type R3fParticleBinding } from './r3f-particle-binding';
22
+ import { physicsBindingsByElement, type R3fPhysicsBinding } from './r3f-physics-binding';
23
+ import { parseAuthoringTsx, refIdentifier } from './ts-ast';
24
+
25
+ export type {
26
+ R3fEnvironmentBinding,
27
+ R3fEnvironmentNumberBinding,
28
+ R3fEnvironmentStringBinding,
29
+ } from './r3f-environment-binding';
30
+ export type {
31
+ R3fJointBinding,
32
+ R3fJointHook,
33
+ R3fJointLiteral,
34
+ R3fJointLiteralRange,
35
+ } from './r3f-joint-binding';
36
+ export type { R3fLodBinding, R3fLodNumberBinding } from './r3f-lod-binding';
37
+ export type {
38
+ QuarksEmitterShape,
39
+ R3fParticleBinding,
40
+ R3fParticleNumberBinding,
41
+ } from './r3f-particle-binding';
42
+ export type { PhysicsChannel, R3fPhysicsBinding } from './r3f-physics-binding';
43
+
44
+ export interface OidEntry {
45
+ file: string;
46
+ line: number; // 1-based
47
+ col: number; // 0-based (matches ts char)
48
+ component: string | null;
49
+ tag: string;
50
+ /**
51
+ * R3F-only, source-proven authoring contract for a LOCAL custom component
52
+ * callsite. `undefined` means the component definition is not in this file
53
+ * (usually an import), so the editor must retain its conservative fallback.
54
+ *
55
+ * A single native root plus forwarded standard group props is ordinary R3F,
56
+ * not a vgai wrapper. Recording it here lets the editor safely ADD an absent
57
+ * transform prop at the callsite: the source analyser has proven where that
58
+ * prop lands. An empty `transformProps` list is an equally useful result —
59
+ * the local component exists, but exposes no writable spatial channel.
60
+ */
61
+ r3fAuthoring?: R3fComponentContract;
62
+ /**
63
+ * The props the tag's component DECLARES — resolved server-side through the
64
+ * TypeScript type checker (`component-prop-types.ts`) and shipped here so
65
+ * the editor can inspect a node by its declared surface rather than by the
66
+ * attributes someone happened to write. Absent for a native tag, or when the
67
+ * definition could not be resolved; the editor then falls back to reading
68
+ * the authored attributes alone.
69
+ */
70
+ props?: ComponentPropSpec[];
71
+ /**
72
+ * True once the server-side declared-prop resolver has ANSWERED for this
73
+ * tag — present even when the answer was "no props". Distinguishes "the
74
+ * declaration rung has not landed yet" (the resolver's cold ts.Program
75
+ * takes seconds and enrichment is async) from "the resolver looked and
76
+ * found nothing": the inspector's no-declaration warning is honest only in
77
+ * the second state. Measured on racing-game: the doctor's sweep inspected
78
+ * before the cold program answered, the fallback warned once-per-key, and
79
+ * the session ledger held six "No declaration covers" rows for props the
80
+ * resolver demonstrably types (`allowSleep:boolean`, `broadphase:enum`, …).
81
+ */
82
+ propsResolved?: true;
83
+ /** Props physically authored on this JSX callsite. Unlike {@link props},
84
+ * which describes the component declaration, this is the byte-level answer
85
+ * needed by instance editing: an override exists only when the attribute is
86
+ * present here, and applying it to the component is safe only when its source
87
+ * value is a literal. */
88
+ authoredProps?: Array<{
89
+ name: string;
90
+ valueText: string;
91
+ literal: boolean;
92
+ }>;
93
+ /**
94
+ * The R3F authorability diagnostics that pertain to THIS element, attached
95
+ * server-side by `currentOidIndex` so they ride the exact same
96
+ * `/__ui-source/index` payload (and therefore the exact same freshness) as
97
+ * every other source-derived field on this entry. Absent when there are
98
+ * none.
99
+ *
100
+ * Two kinds land here, both selected by `fileDiagnosticJoin`:
101
+ * the diagnostic recorded AT this element (an R3F004 "has no name" on this
102
+ * very callsite), and the ones recorded against the component whose body
103
+ * lexically contains it (R3F002/R3F003/R3F005 — `component` matches). The
104
+ * second is what carries a definition-side warning out to the rows that
105
+ * INSTANTIATE that component: an instance's boundary object also carries its
106
+ * definition-root oid, so the client reads both entries. See
107
+ * `r3f-diagnostic-index.ts`.
108
+ */
109
+ diagnostics?: R3fAuthoringDiagnostic[];
110
+ /**
111
+ * The oid of the JSX element that lexically ENCLOSES this one, in the same
112
+ * file. Recorded because a wrapper tag does not always become its own node:
113
+ * `<RigidBody>` from `@react-three/rapier` never forwards the editor's stamp
114
+ * to the Object3D it renders, so it has no live object to select — yet its
115
+ * props (`type`, `gravityScale`, colliders) are exactly the configuration an
116
+ * author expects to find on the thing inside it. This chain is what lets the
117
+ * inspector attribute a collapsed wrapper's props to the node it wraps.
118
+ */
119
+ parentOid?: string;
120
+ /**
121
+ * The PHYSICS BODY that owns this element's transform, when a simulation
122
+ * binding attached one to its ref (`r3f-physics-binding.ts`).
123
+ *
124
+ * Present ⇒ this element's own `position`/`rotation`/`scale` props are dead
125
+ * on arrival: the body writes the object's matrix every frame from ITS OWN
126
+ * spawn, so a write here produces source that contradicts the running world.
127
+ * Every writer reads it as a refusal that can name the binding, and the
128
+ * component contract reads its `forwarded` channels as real forwarding —
129
+ * which is what routes the write to the callsite literal the hook reads.
130
+ */
131
+ physicsBinding?: R3fPhysicsBinding;
132
+ /** Native Rapier joint hooks that name this `<RigidBody ref={…}>` as one
133
+ * endpoint. Hook parameters remain the project's TSX source of truth. */
134
+ jointBindings?: readonly R3fJointBinding[];
135
+ /** Native three.quarks shape construction bound to this emitter primitive. */
136
+ particleBinding?: R3fParticleBinding;
137
+ /** Drei Detailed is a source projection of the native THREE.LOD it creates. */
138
+ lodBinding?: R3fLodBinding;
139
+ /** Native Fiber scene attachment (`background` or `fog`). */
140
+ environmentBinding?: R3fEnvironmentBinding;
141
+ }
142
+
143
+ /**
144
+ * One R3F authoring-convention warning.
145
+ *
146
+ * DECLARED HERE, not in its producer. `r3f-project-contracts.ts` computes
147
+ * these (and re-exports this type for its existing importers), but that module
148
+ * imports `node:fs` — while this one is deliberately dependency-free so a
149
+ * PAGE can import it. Since a diagnostic now travels ON an `OidEntry` (see its
150
+ * `diagnostics` field), the TYPE has to live on the browser-safe side of that
151
+ * line even though the ANALYSIS does not.
152
+ */
153
+ export interface R3fAuthoringDiagnostic {
154
+ code: 'R3F002' | 'R3F003' | 'R3F004' | 'R3F005';
155
+ severity: 'warning';
156
+ file: string;
157
+ /** 1-based, and computed from `node.getStart()` — the SAME position basis
158
+ * `OidEntry.line`/`col` use, which is what makes an exact positional join
159
+ * between the two sound rather than approximate. */
160
+ line: number;
161
+ col: number;
162
+ /** The component the warning is ABOUT: the definition it names (R3F002/3/5)
163
+ * or the tag at the offending callsite (R3F004). */
164
+ component: string;
165
+ message: string;
166
+ }
167
+
168
+ /**
169
+ * A prop as DECLARED by a component, independent of any callsite. Lives here
170
+ * beside `OidEntry` because it travels on it; it is PRODUCED by
171
+ * `component-prop-types.ts` (server-side, needs a `ts.Program`) and CONSUMED
172
+ * by the editor's authoring adapters.
173
+ */
174
+ export interface ComponentPropSpec {
175
+ name: string;
176
+ /** Inspector widget implied by the declared type; `null` when no widget
177
+ * honestly represents it (the consumer falls back to the attribute text). */
178
+ type: 'string' | 'number' | 'boolean' | 'enum' | 'vec3' | 'json' | null;
179
+ /** Allowed values, for a union of string literals. */
180
+ options?: Array<string | number>;
181
+ /** Declared `?:`, or given a default by the component's destructuring. */
182
+ optional: boolean;
183
+ /** The default's source text (`1.55`, `'raider'`, `defaultSpawn()`). */
184
+ defaultText?: string;
185
+ /** The default as a value, when the default is a literal. */
186
+ defaultValue?: string | number | boolean | number[];
187
+ /** The prop's jsdoc, as one line. */
188
+ doc?: string;
189
+ }
190
+
191
+ function literalJsxExpression(expression: ts.Expression | undefined): boolean {
192
+ if (!expression) return false;
193
+ if (
194
+ ts.isStringLiteral(expression) ||
195
+ ts.isNumericLiteral(expression) ||
196
+ expression.kind === ts.SyntaxKind.TrueKeyword ||
197
+ expression.kind === ts.SyntaxKind.FalseKeyword
198
+ ) {
199
+ return true;
200
+ }
201
+ if (!ts.isArrayLiteralExpression(expression)) return false;
202
+ return expression.elements.every(
203
+ (element) =>
204
+ ts.isNumericLiteral(element) ||
205
+ (ts.isPrefixUnaryExpression(element) &&
206
+ element.operator === ts.SyntaxKind.MinusToken &&
207
+ ts.isNumericLiteral(element.operand)),
208
+ );
209
+ }
210
+
211
+ function authoredPropsOf(
212
+ node: ts.JsxOpeningElement | ts.JsxSelfClosingElement,
213
+ sourceFile: ts.SourceFile,
214
+ ): NonNullable<OidEntry['authoredProps']> {
215
+ return node.attributes.properties.flatMap((property) => {
216
+ if (!ts.isJsxAttribute(property)) return [];
217
+ const name = property.name.getText(sourceFile);
218
+ const initializer = property.initializer;
219
+ if (!initializer) return [{ name, valueText: 'true', literal: true }];
220
+ if (ts.isStringLiteral(initializer)) {
221
+ return [{ name, valueText: initializer.getText(sourceFile), literal: true }];
222
+ }
223
+ if (!ts.isJsxExpression(initializer)) return [];
224
+ return [
225
+ {
226
+ name,
227
+ valueText: initializer.expression?.getText(sourceFile) ?? '',
228
+ literal: literalJsxExpression(initializer.expression),
229
+ },
230
+ ];
231
+ });
232
+ }
233
+
234
+ /** Persistent oid store: signature -> oid (kept stable across re-transforms). */
235
+ export class OidStore {
236
+ private bySig = new Map<string, string>();
237
+ private counter = 0;
238
+ readonly index = new Map<string, OidEntry>();
239
+
240
+ getOrCreate(sig: string): string {
241
+ let oid = this.bySig.get(sig);
242
+ if (!oid) {
243
+ this.counter += 1;
244
+ oid = `o${this.counter.toString(36)}${hash(sig).toString(36)}`;
245
+ this.bySig.set(sig, oid);
246
+ }
247
+ return oid;
248
+ }
249
+
250
+ /** Clear index entries for a file before re-transforming it. */
251
+ clearFile(file: string): void {
252
+ for (const [oid, e] of this.index) if (e.file === file) this.index.delete(oid);
253
+ }
254
+ }
255
+
256
+ function hash(s: string): number {
257
+ let h = 0;
258
+ for (let i = 0; i < s.length; i++) h = (Math.imul(31, h) + s.charCodeAt(i)) | 0;
259
+ return Math.abs(h);
260
+ }
261
+
262
+ interface SourceEdit {
263
+ pos: number;
264
+ end?: number;
265
+ text: string;
266
+ }
267
+
268
+ export type R3fTransformProp = 'position' | 'rotation' | 'scale';
269
+
270
+ export interface R3fComponentContract {
271
+ root: 'single' | 'multiple' | 'non-spatial' | 'unknown';
272
+ rootTag?: string;
273
+ transformProps: R3fTransformProp[];
274
+ /** The callsite's native `visible` prop is proven to reach the single
275
+ * Object3D root. Omitted is deliberately false/unknown, so an editor never
276
+ * writes a decorative prop that the component silently ignores. */
277
+ visibleProp?: true;
278
+ /**
279
+ * The component's own source says the SIMULATION owns this transform: it
280
+ * neither accepts nor spreads a transform prop, and it writes its root's
281
+ * `position`/`rotation`/`quaternion`/`scale` through a ref every frame
282
+ * inside `useFrame`.
283
+ *
284
+ * Present (and only ever `true`) when both halves hold, so a contract that
285
+ * predates the distinction compares equal to one that has no opinion.
286
+ *
287
+ * This is a DECLARATION OF NON-AUTHORABILITY, never of authorability:
288
+ * `transformProps` stays empty, so every writer path
289
+ * (`transformEditability`) still refuses the edit — with an honest reason
290
+ * instead of "does not forward position". What it buys is silence from the
291
+ * convention diagnostics whose premise it falsifies (R3F002 asks the
292
+ * component to forward an authored transform the simulation would overwrite
293
+ * on the next tick; R3F005 warns that runtime motion fights an editor
294
+ * transform that cannot exist here).
295
+ */
296
+ simulationOwnedTransform?: boolean;
297
+ /**
298
+ * The subset of {@link transformProps} this component forwards to a PHYSICS
299
+ * BODY BINDING rather than to its native root — the channels whose callsite
300
+ * literal is read by a `useBox(…)`-family hook (`r3f-physics-binding.ts`).
301
+ *
302
+ * The write is exactly as real as any other forwarded prop, which is why
303
+ * these are IN `transformProps`; what this field adds is WHY it is real, and
304
+ * that fact has a consequence no other prop has: a body re-spawns from this
305
+ * literal and then owns the node's matrix, so the authored value must survive
306
+ * the re-settle after a remount. That is the `physics-binding`
307
+ * {@link WriteAnchorKind}'s own contract, and this is the source fact the
308
+ * planner reads to classify an anchor as one.
309
+ *
310
+ * Absent (never `[]`) when nothing is forwarded to a binding, so a contract
311
+ * that predates the distinction compares equal to one that has no opinion —
312
+ * the same rule `simulationOwnedTransform` above follows, and one the
313
+ * fixed-point loop in `collectR3fComponentContracts` depends on.
314
+ */
315
+ bodyForwarded?: R3fTransformProp[];
316
+ }
317
+
318
+ const R3F_TRANSFORM_PROPS = ['position', 'rotation', 'scale'] as const;
319
+
320
+ /**
321
+ * Contracts for components vgai does NOT own, keyed by package specifier and
322
+ * export name. The project resolver reads a local component's contract out of
323
+ * its own source; an installed package has no source to walk from the browser
324
+ * tier and no reason to expect one, so the few whose native root is part of the
325
+ * package's documented public API are recorded here instead.
326
+ *
327
+ * Deliberately tiny and hand-audited. Everything absent from this table stays
328
+ * conservative (`root: 'unknown'`), because guessing at a dependency's scene
329
+ * ownership is how an editor writes a prop the library silently ignores.
330
+ *
331
+ * `@react-three/rapier`'s `<RigidBody>` renders one host `object3D` and passes
332
+ * `position`/`rotation`/`scale` straight to it — the body's initial transform is
333
+ * read back off that object, and rapier scales the colliders it owns by the
334
+ * object's world scale — so a transform written at the callsite lands exactly
335
+ * where the editor claims it does.
336
+ *
337
+ * `rootTag` is `group`, not `object3D`, because the tag is proved against the
338
+ * CALLER's props type: `typeAllowsStandardRootProps` looks for the literal text
339
+ * `ThreeElements['<rootTag>']`, and a component forwarding to a RigidBody types
340
+ * its props as `ThreeElements['group'] & …` — the ordinary R3F spelling for "a
341
+ * spatial container". `object3D` would match no real caller.
342
+ */
343
+ const BUILTIN_R3F_CONTRACTS: Readonly<
344
+ Record<string, Readonly<Record<string, R3fComponentContract>>>
345
+ > = {
346
+ '@react-three/rapier': {
347
+ RigidBody: { root: 'single', rootTag: 'group', transformProps: [...R3F_TRANSFORM_PROPS] },
348
+ },
349
+ };
350
+
351
+ /**
352
+ * Package prop types that are themselves a proved native-root prop surface.
353
+ * Kept beside the component contract because both facts come from the same
354
+ * hand-audited public API. This lets ordinary wrappers preserve the package's
355
+ * own type (`Omit<RigidBodyProps, ...>`) without pretending every imported
356
+ * type is spatial.
357
+ */
358
+ const BUILTIN_R3F_ROOT_PROP_TYPES: Readonly<Record<string, Readonly<Record<string, string>>>> = {
359
+ '@react-three/rapier': { RigidBodyProps: 'group' },
360
+ };
361
+
362
+ /** Local imported type binding → native root tag it proves. */
363
+ function builtinRootPropTypes(sf: ts.SourceFile): Map<string, string> {
364
+ const bound = new Map<string, string>();
365
+ for (const statement of sf.statements) {
366
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
367
+ continue;
368
+ }
369
+ const known = BUILTIN_R3F_ROOT_PROP_TYPES[statement.moduleSpecifier.text];
370
+ const bindings = statement.importClause?.namedBindings;
371
+ if (!known || !bindings || !ts.isNamedImports(bindings)) continue;
372
+ for (const element of bindings.elements) {
373
+ const importedName = element.propertyName?.text ?? element.name.text;
374
+ const rootTag = known[importedName];
375
+ if (rootTag) bound.set(element.name.text, rootTag);
376
+ }
377
+ }
378
+ return bound;
379
+ }
380
+
381
+ /** True for a contract that came from the table above rather than from source.
382
+ * Identity, not shape: a project-local definition that SHADOWS an imported
383
+ * binding produces its own freshly analysed object, so it is never mistaken
384
+ * for the package's. */
385
+ export function isBuiltinR3fContract(contract: R3fComponentContract | undefined): boolean {
386
+ if (!contract) return false;
387
+ return Object.values(BUILTIN_R3F_CONTRACTS).some((exports) =>
388
+ Object.values(exports).includes(contract),
389
+ );
390
+ }
391
+
392
+ /** The built-in contract for one named export of an external package, if any. */
393
+ export function builtinR3fContract(
394
+ specifier: string,
395
+ exportName: string,
396
+ ): R3fComponentContract | undefined {
397
+ return BUILTIN_R3F_CONTRACTS[specifier]?.[exportName];
398
+ }
399
+
400
+ /** The local bindings ONE import declaration takes from the built-in table.
401
+ * Keyed by the LOCAL name, so `import { RigidBody as RB }` registers `RB`. */
402
+ export function builtinR3fContractsOfImport(
403
+ declaration: ts.ImportDeclaration,
404
+ ): Map<string, R3fComponentContract> {
405
+ const bound = new Map<string, R3fComponentContract>();
406
+ if (!ts.isStringLiteral(declaration.moduleSpecifier)) return bound;
407
+ const bindings = declaration.importClause?.namedBindings;
408
+ if (!bindings || !ts.isNamedImports(bindings)) return bound;
409
+ for (const element of bindings.elements) {
410
+ const contract = builtinR3fContract(
411
+ declaration.moduleSpecifier.text,
412
+ element.propertyName?.text ?? element.name.text,
413
+ );
414
+ if (contract) bound.set(element.name.text, contract);
415
+ }
416
+ return bound;
417
+ }
418
+
419
+ /** Every built-in contract one module's imports bring into scope. The browser
420
+ * tier has no filesystem to resolve project-local imports from, so this is the
421
+ * whole of the contract knowledge available to it. */
422
+ export function builtinR3fContractsForSource(
423
+ code: string,
424
+ file: string,
425
+ ): Map<string, R3fComponentContract> {
426
+ const sf = parseAuthoringTsx(file, code);
427
+ const contracts = new Map<string, R3fComponentContract>();
428
+ for (const statement of sf.statements) {
429
+ if (!ts.isImportDeclaration(statement)) continue;
430
+ for (const [name, contract] of builtinR3fContractsOfImport(statement)) {
431
+ contracts.set(name, contract);
432
+ }
433
+ }
434
+ return contracts;
435
+ }
436
+
437
+ function unwrapExpression(expression: ts.Expression): ts.Expression {
438
+ let current = expression;
439
+ while (
440
+ ts.isParenthesizedExpression(current) ||
441
+ ts.isAsExpression(current) ||
442
+ ts.isSatisfiesExpression(current)
443
+ ) {
444
+ current = current.expression;
445
+ }
446
+ return current;
447
+ }
448
+
449
+ function returnedExpression(body: ts.ConciseBody): ts.Expression | undefined {
450
+ if (!ts.isBlock(body)) return unwrapExpression(body);
451
+ const returns = body.statements.filter(ts.isReturnStatement);
452
+ if (returns.length !== 1 || !returns[0]?.expression) return undefined;
453
+ return unwrapExpression(returns[0].expression);
454
+ }
455
+
456
+ /**
457
+ * A type alias and the file it was written in.
458
+ *
459
+ * These travel together because `ts.Node.getText(sf)` reads the node's
460
+ * position range out of the SOURCE TEXT it is handed: give it another file's
461
+ * `SourceFile` and it returns whatever characters happen to sit at those
462
+ * offsets. An alias resolved across a module boundary is exactly that case.
463
+ */
464
+ export interface R3fTypeAlias {
465
+ readonly type: ts.TypeNode;
466
+ readonly sf: ts.SourceFile;
467
+ }
468
+
469
+ /** The `export type X = …` declarations of one module, for importers. */
470
+ export function exportedR3fTypeAliases(sf: ts.SourceFile): Map<string, R3fTypeAlias> {
471
+ const aliases = new Map<string, R3fTypeAlias>();
472
+ for (const statement of sf.statements) {
473
+ if (!ts.isTypeAliasDeclaration(statement)) continue;
474
+ if (!ts.getModifiers(statement)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) continue;
475
+ aliases.set(statement.name.text, { type: statement.type, sf });
476
+ }
477
+ return aliases;
478
+ }
479
+
480
+ function textIncludesBuiltinRootProps(
481
+ text: string,
482
+ rootTag: string,
483
+ builtinPropTypes: ReadonlyMap<string, string>,
484
+ ): boolean {
485
+ for (const [name, knownRootTag] of builtinPropTypes) {
486
+ if (knownRootTag === rootTag && new RegExp(`\\b${name}\\b`).test(text)) return true;
487
+ }
488
+ return false;
489
+ }
490
+
491
+ function typeAllowsStandardRootProps(
492
+ type: ts.TypeNode | undefined,
493
+ rootTag: string,
494
+ sf: ts.SourceFile,
495
+ typeAliases: ReadonlyMap<string, R3fTypeAlias>,
496
+ builtinPropTypes: ReadonlyMap<string, string>,
497
+ seen = new Set<string>(),
498
+ ): boolean {
499
+ if (!type) return false;
500
+ const text = type.getText(sf);
501
+ const escapedRootTag = rootTag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
502
+ if (new RegExp(`\\bThreeElements\\s*\\[\\s*['"]${escapedRootTag}['"]\\s*\\]`).test(text))
503
+ return true;
504
+ if (textIncludesBuiltinRootProps(text, rootTag, builtinPropTypes)) return true;
505
+ if (ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName)) {
506
+ const name = type.typeName.text;
507
+ if (seen.has(name)) return false;
508
+ const alias = typeAliases.get(name);
509
+ if (!alias) return false;
510
+ seen.add(name);
511
+ return typeAllowsStandardRootProps(
512
+ alias.type,
513
+ rootTag,
514
+ alias.sf,
515
+ typeAliases,
516
+ builtinPropTypes,
517
+ seen,
518
+ );
519
+ }
520
+ for (const [name, alias] of typeAliases) {
521
+ if (seen.has(name) || !new RegExp(`\\b${name}\\b`).test(text)) continue;
522
+ const nextSeen = new Set(seen).add(name);
523
+ if (
524
+ typeAllowsStandardRootProps(
525
+ alias.type,
526
+ rootTag,
527
+ alias.sf,
528
+ typeAliases,
529
+ builtinPropTypes,
530
+ nextSeen,
531
+ )
532
+ )
533
+ return true;
534
+ }
535
+ return false;
536
+ }
537
+
538
+ function spatialRootOpening(expression: ts.Expression): ts.JsxOpeningLikeElement | undefined {
539
+ if (ts.isJsxSelfClosingElement(expression)) return expression;
540
+ if (!ts.isJsxElement(expression)) return undefined;
541
+ return expression.openingElement;
542
+ }
543
+
544
+ type FragmentRootCardinality = 0 | 1 | 'multiple' | 'unknown';
545
+
546
+ function combineFragmentRootCardinality(
547
+ left: FragmentRootCardinality,
548
+ right: FragmentRootCardinality,
549
+ ): FragmentRootCardinality {
550
+ if (left === 'multiple' || right === 'multiple') return 'multiple';
551
+ if (left === 'unknown' || right === 'unknown') return 'unknown';
552
+ return left + right >= 2 ? 'multiple' : ((left + right) as 0 | 1);
553
+ }
554
+
555
+ function jsxRootCardinality(
556
+ element: ts.JsxElement | ts.JsxSelfClosingElement,
557
+ sf: ts.SourceFile,
558
+ knownContracts: ReadonlyMap<string, R3fComponentContract>,
559
+ ): FragmentRootCardinality {
560
+ const opening = ts.isJsxElement(element) ? element.openingElement : element;
561
+ const tag = opening.tagName.getText(sf);
562
+ if (!/^[A-Z]/.test(tag)) return isNonSpatialNativeTag(tag) ? 0 : 1;
563
+ const delegated = knownContracts.get(tag);
564
+ if (!delegated || delegated.root === 'unknown') return 'unknown';
565
+ if (delegated.root === 'non-spatial') return 0;
566
+ return delegated.root === 'multiple' ? 'multiple' : 1;
567
+ }
568
+
569
+ function mappedRootCardinality(
570
+ expression: ts.CallExpression,
571
+ sf: ts.SourceFile,
572
+ knownContracts: ReadonlyMap<string, R3fComponentContract>,
573
+ ): FragmentRootCardinality | undefined {
574
+ if (
575
+ !ts.isPropertyAccessExpression(expression.expression) ||
576
+ expression.expression.name.text !== 'map'
577
+ ) {
578
+ return undefined;
579
+ }
580
+ const callback = expression.arguments[0];
581
+ if (!callback || (!ts.isArrowFunction(callback) && !ts.isFunctionExpression(callback))) {
582
+ return 'unknown';
583
+ }
584
+ const returned = returnedExpression(callback.body);
585
+ if (!returned) return 'unknown';
586
+ const item = expressionRootCardinality(returned, sf, knownContracts);
587
+ // A map can emit any number of its item. It remains non-spatial only when
588
+ // the item is proved non-spatial; every spatial item makes a plural result
589
+ // possible, which is precisely the non-integral component shape R3F003
590
+ // reports.
591
+ return item === 0 ? 0 : item === 'unknown' ? 'unknown' : 'multiple';
592
+ }
593
+
594
+ function expressionRootCardinality(
595
+ expression: ts.Expression,
596
+ sf: ts.SourceFile,
597
+ knownContracts: ReadonlyMap<string, R3fComponentContract>,
598
+ ): FragmentRootCardinality {
599
+ const unwrapped = unwrapExpression(expression);
600
+ if (unwrapped.kind === ts.SyntaxKind.NullKeyword) return 0;
601
+ if (unwrapped.kind === ts.SyntaxKind.FalseKeyword) return 0;
602
+ if (ts.isJsxFragment(unwrapped)) {
603
+ return fragmentRootCardinality(unwrapped, sf, knownContracts);
604
+ }
605
+ if (ts.isJsxElement(unwrapped) || ts.isJsxSelfClosingElement(unwrapped)) {
606
+ return jsxRootCardinality(unwrapped, sf, knownContracts);
607
+ }
608
+ if (ts.isCallExpression(unwrapped)) {
609
+ return mappedRootCardinality(unwrapped, sf, knownContracts) ?? 'unknown';
610
+ }
611
+ return 'unknown';
612
+ }
613
+
614
+ function jsxChildRootCardinality(
615
+ child: ts.JsxChild,
616
+ sf: ts.SourceFile,
617
+ knownContracts: ReadonlyMap<string, R3fComponentContract>,
618
+ ): FragmentRootCardinality {
619
+ if (ts.isJsxText(child)) return child.text.trim() === '' ? 0 : 'unknown';
620
+ if (ts.isJsxExpression(child)) {
621
+ return child.expression ? expressionRootCardinality(child.expression, sf, knownContracts) : 0;
622
+ }
623
+ if (ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child)) {
624
+ return jsxRootCardinality(child, sf, knownContracts);
625
+ }
626
+ if (ts.isJsxFragment(child)) return fragmentRootCardinality(child, sf, knownContracts);
627
+ return 'unknown';
628
+ }
629
+
630
+ function fragmentRootCardinality(
631
+ fragment: ts.JsxFragment,
632
+ sf: ts.SourceFile,
633
+ knownContracts: ReadonlyMap<string, R3fComponentContract>,
634
+ ): FragmentRootCardinality {
635
+ let result: FragmentRootCardinality = 0;
636
+ for (const child of fragment.children) {
637
+ result = combineFragmentRootCardinality(
638
+ result,
639
+ jsxChildRootCardinality(child, sf, knownContracts),
640
+ );
641
+ }
642
+ return result;
643
+ }
644
+
645
+ function fragmentComponentContract(
646
+ fragment: ts.JsxFragment,
647
+ sf: ts.SourceFile,
648
+ knownContracts: ReadonlyMap<string, R3fComponentContract>,
649
+ ): R3fComponentContract {
650
+ const cardinality = fragmentRootCardinality(fragment, sf, knownContracts);
651
+ if (cardinality === 0) return { root: 'non-spatial', transformProps: [] };
652
+ if (cardinality === 'multiple') return { root: 'multiple', transformProps: [] };
653
+ // A fragment with one proved spatial child, or an unresolved child, has no
654
+ // fragment object of its own through which transform props can be proved to
655
+ // flow. Keep it conservative without falsely claiming several roots.
656
+ return { root: 'unknown', transformProps: [] };
657
+ }
658
+
659
+ /**
660
+ * Native R3F tags that are NOT `Object3D`s.
661
+ *
662
+ * R3F's host namespace is all of three's constructors, not just the scene
663
+ * graph: materials, geometries and buffer attributes are elements too, and
664
+ * they mount by `attach` onto a parent rather than by taking a place in the
665
+ * hierarchy. They have no `position`, no `rotation`, no `scale`, and they are
666
+ * never a hierarchy row.
667
+ *
668
+ * A component whose single root is one of these is a perfectly ordinary R3F
669
+ * idiom — a shared material helper returning `<meshStandardMaterial …/>` is
670
+ * about as common as R3F code gets — and it must not be read as an
671
+ * unauthorable spatial instance. R3F002 would demand it forward transform
672
+ * channels the element does not have (there is no `position` prop on a
673
+ * material to accept), and R3F004 would ask for a `name` "so its instance is
674
+ * recognizable in the hierarchy" when nothing about it ever appears there.
675
+ * Both are unactionable by construction, which is precisely the failure the
676
+ * `simulationOwnedTransform` and keyed-list-item carve-outs already exist to
677
+ * prevent elsewhere in these rules.
678
+ *
679
+ * Recognized by suffix because three's naming is regular here and the set is
680
+ * open — every release adds materials and geometries, and an exhaustive list
681
+ * would silently rot into false positives again. The three exact names are the
682
+ * scene-level attachables that carry no suffix.
683
+ */
684
+ function isNonSpatialNativeTag(tag: string): boolean {
685
+ if (/^[A-Z]/.test(tag)) return false;
686
+ return (
687
+ /(Material|Geometry|Attribute)$/.test(tag) ||
688
+ tag === 'fog' ||
689
+ tag === 'fogExp2' ||
690
+ tag === 'color'
691
+ );
692
+ }
693
+
694
+ interface R3fPropForwarding {
695
+ standardRootProps: boolean;
696
+ wholePropsName?: string;
697
+ restPropsName?: string;
698
+ localBindingByProp: Map<R3fObjectProp, string>;
699
+ }
700
+
701
+ type R3fObjectProp = R3fTransformProp | 'visible';
702
+
703
+ function isR3fTransformProp(value: string): value is R3fTransformProp {
704
+ return (R3F_TRANSFORM_PROPS as readonly string[]).includes(value);
705
+ }
706
+
707
+ function isR3fObjectProp(value: string): value is R3fObjectProp {
708
+ return isR3fTransformProp(value) || value === 'visible';
709
+ }
710
+
711
+ function propForwarding(
712
+ first: ts.ParameterDeclaration | undefined,
713
+ rootTag: string,
714
+ sf: ts.SourceFile,
715
+ typeAliases: ReadonlyMap<string, R3fTypeAlias>,
716
+ builtinPropTypes: ReadonlyMap<string, string>,
717
+ ): R3fPropForwarding {
718
+ const result: R3fPropForwarding = {
719
+ standardRootProps: typeAllowsStandardRootProps(
720
+ first?.type,
721
+ rootTag,
722
+ sf,
723
+ typeAliases,
724
+ builtinPropTypes,
725
+ ),
726
+ localBindingByProp: new Map(),
727
+ };
728
+ if (!first) return result;
729
+ if (ts.isIdentifier(first.name)) {
730
+ result.wholePropsName = first.name.text;
731
+ return result;
732
+ }
733
+ if (!ts.isObjectBindingPattern(first.name)) return result;
734
+ for (const element of first.name.elements) {
735
+ if (element.dotDotDotToken && ts.isIdentifier(element.name)) {
736
+ result.restPropsName = element.name.text;
737
+ continue;
738
+ }
739
+ if (!ts.isIdentifier(element.name)) continue;
740
+ const authoredName = element.propertyName?.getText(sf) ?? element.name.text;
741
+ if (isR3fObjectProp(authoredName)) {
742
+ result.localBindingByProp.set(authoredName, element.name.text);
743
+ }
744
+ }
745
+ return result;
746
+ }
747
+
748
+ /**
749
+ * Is this parameter's type spelled AS another component's props — the guard
750
+ * wrapper's `Parameters<typeof Body>[0]` / `ComponentProps<typeof Body>`?
751
+ *
752
+ * Deliberately a spelling test and not a type check: this analyzer holds no
753
+ * checker, and the one thing it needs to know is that the wrapper's props ARE
754
+ * the delegate's, which the spelling states outright.
755
+ */
756
+ function typeIsDelegateProps(
757
+ type: ts.TypeNode | undefined,
758
+ delegateTag: string,
759
+ sf: ts.SourceFile,
760
+ ): boolean {
761
+ if (!type) return false;
762
+ const text = type.getText(sf).replace(/\s+/g, '');
763
+ const tag = delegateTag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
764
+ return new RegExp(
765
+ `^(React\\.)?(Parameters<typeof${tag}>\\[0\\]|ComponentProps(WithoutRef|WithRef)?<typeof${tag}>)`,
766
+ ).test(text);
767
+ }
768
+
769
+ function spreadForwardsAll(
770
+ property: ts.JsxSpreadAttribute,
771
+ forwarding: R3fPropForwarding,
772
+ ): boolean {
773
+ if (!forwarding.standardRootProps || !ts.isIdentifier(property.expression)) return false;
774
+ return (
775
+ property.expression.text === forwarding.wholePropsName ||
776
+ property.expression.text === forwarding.restPropsName
777
+ );
778
+ }
779
+
780
+ function attributeForwardsObjectProp(
781
+ property: ts.JsxAttribute,
782
+ forwarding: R3fPropForwarding,
783
+ sf: ts.SourceFile,
784
+ ): R3fObjectProp | undefined {
785
+ const propName = property.name.getText(sf);
786
+ if (!isR3fObjectProp(propName)) return undefined;
787
+ if (!property.initializer || !ts.isJsxExpression(property.initializer)) return undefined;
788
+ const value = property.initializer.expression;
789
+ if (!value) return undefined;
790
+ const localName = forwarding.localBindingByProp.get(propName);
791
+ if (localName && ts.isIdentifier(value) && value.text === localName) return propName;
792
+ if (
793
+ forwarding.wholePropsName &&
794
+ ts.isPropertyAccessExpression(value) &&
795
+ ts.isIdentifier(value.expression) &&
796
+ value.expression.text === forwarding.wholePropsName &&
797
+ value.name.text === propName
798
+ ) {
799
+ return propName;
800
+ }
801
+ return undefined;
802
+ }
803
+
804
+ function forwardedObjectProps(
805
+ opening: ts.JsxOpeningLikeElement,
806
+ forwarding: R3fPropForwarding,
807
+ sf: ts.SourceFile,
808
+ ): R3fObjectProp[] {
809
+ const forwarded = new Set<R3fObjectProp>();
810
+ for (const property of opening.attributes.properties) {
811
+ if (ts.isJsxSpreadAttribute(property)) {
812
+ if (spreadForwardsAll(property, forwarding)) {
813
+ for (const channel of R3F_TRANSFORM_PROPS) forwarded.add(channel);
814
+ forwarded.add('visible');
815
+ }
816
+ continue;
817
+ }
818
+ const prop = attributeForwardsObjectProp(property, forwarding, sf);
819
+ if (prop) forwarded.add(prop);
820
+ }
821
+ const ordered: readonly R3fObjectProp[] = [...R3F_TRANSFORM_PROPS, 'visible'];
822
+ return ordered.filter((prop) => forwarded.has(prop));
823
+ }
824
+
825
+ /**
826
+ * A ref whose `.current` transform is written inside a `useFrame` body — the
827
+ * source-visible mark of runtime-driven motion.
828
+ *
829
+ * Optional chaining is part of the pattern, not an edge case: `useRef<
830
+ * THREE.Group>(null)` types `.current` as nullable, so `body.current?.position
831
+ * .copy(…)` is the ordinary spelling and `body.current.position.copy(…)` the
832
+ * non-null-asserted one. Both must read the same.
833
+ */
834
+ const RUNTIME_MOTION_MUTATION_RE =
835
+ /\b([A-Za-z_$][\w$]*)\.current\??\.(?:position|rotation|quaternion|scale)(?:\??\.[xyzw]+)?\s*(?:[+\-*/]?=|\??\.(?:set|setScalar|copy|add|sub|multiply|lerp|slerp|fromArray)\s*\()/g;
836
+
837
+ /**
838
+ * The refs any `useFrame` callback in `scope` drives every frame.
839
+ *
840
+ * Deliberately a regex over the callback's text rather than a resolved
841
+ * dataflow analysis — the same honesty budget the rest of this module's source
842
+ * evidence holds to, and the single derivation BOTH readers use (the contract
843
+ * below, and R3F005 in `r3f-project-contracts.ts`) so the two can never
844
+ * disagree about whether one component drives its own root.
845
+ */
846
+ export function runtimeMotionRefs(scope: ts.Node, sf: ts.SourceFile): Set<string> {
847
+ const refs = new Set<string>();
848
+ const visit = (node: ts.Node): void => {
849
+ if (ts.isCallExpression(node) && /(^|\.)useFrame$/.test(node.expression.getText(sf))) {
850
+ for (const match of node.getText(sf).matchAll(RUNTIME_MOTION_MUTATION_RE)) {
851
+ refs.add(match[1]!);
852
+ }
853
+ }
854
+ ts.forEachChild(node, visit);
855
+ };
856
+ visit(scope);
857
+ return refs;
858
+ }
859
+
860
+ /** `{ simulationOwnedTransform: true }` when this root's transform is written
861
+ * by the runtime and by nothing else — see the field's own doc. Only when
862
+ * NOTHING is forwarded: a component that forwards a channel and then animates
863
+ * it is the genuine conflict R3F005 exists for, not a simulation-owned
864
+ * instance. */
865
+ function simulationOwnership(
866
+ opening: ts.JsxOpeningLikeElement,
867
+ transformProps: readonly R3fTransformProp[],
868
+ motionRefs: ReadonlySet<string>,
869
+ ): { simulationOwnedTransform?: true } {
870
+ if (transformProps.length > 0) return {};
871
+ const rootRef = refIdentifier(opening);
872
+ return rootRef && motionRefs.has(rootRef) ? { simulationOwnedTransform: true } : {};
873
+ }
874
+
875
+ function componentContract(
876
+ parameters: ts.NodeArray<ts.ParameterDeclaration>,
877
+ body: ts.ConciseBody,
878
+ sf: ts.SourceFile,
879
+ typeAliases: ReadonlyMap<string, R3fTypeAlias>,
880
+ builtinPropTypes: ReadonlyMap<string, string>,
881
+ knownContracts: ReadonlyMap<string, R3fComponentContract>,
882
+ motionRefs: ReadonlySet<string>,
883
+ physicsBindings: ReadonlyMap<ts.JsxOpeningLikeElement, R3fPhysicsBinding>,
884
+ ): R3fComponentContract {
885
+ const expression = returnedExpression(body);
886
+ if (!expression) return { root: 'unknown', transformProps: [] };
887
+ if (expression.kind === ts.SyntaxKind.NullKeyword)
888
+ return { root: 'non-spatial', transformProps: [] };
889
+ if (ts.isJsxFragment(expression))
890
+ return fragmentComponentContract(expression, sf, knownContracts);
891
+ if (!ts.isJsxElement(expression) && !ts.isJsxSelfClosingElement(expression)) {
892
+ return { root: 'unknown', transformProps: [] };
893
+ }
894
+
895
+ const opening = spatialRootOpening(expression);
896
+ if (!opening) return { root: 'unknown', transformProps: [] };
897
+ const rootTag = opening.tagName.getText(sf);
898
+ // A material/geometry/attribute root is not a spatial instance at all — see
899
+ // `isNonSpatialNativeTag`. Classified here rather than suppressed at each
900
+ // rule, so every rule that keys off `root === 'single'` gets it at once.
901
+ if (isNonSpatialNativeTag(rootTag)) return { root: 'non-spatial', transformProps: [] };
902
+ const delegated = /^[A-Z]/.test(rootTag) ? knownContracts.get(rootTag) : undefined;
903
+ if (/^[A-Z]/.test(rootTag) && !delegated) return { root: 'unknown', transformProps: [] };
904
+ if (delegated && delegated.root !== 'single') return delegated;
905
+ const nativeRootTag = delegated?.rootTag ?? rootTag;
906
+ // A channel this component hands to the PHYSICS BINDING that places its root
907
+ // is forwarded exactly as truly as one written onto the root's own tag — the
908
+ // body is what puts the object in the world, so the callsite literal the hook
909
+ // reads IS this instance's placement (`r3f-physics-binding.ts`). Without this
910
+ // the contract said `Ramp` "does not forward position to its native root"
911
+ // and refused `<Ramp position={[2, -1, 168.55]} />`, the one literal in the
912
+ // whole chain that actually places the ramp.
913
+ const rootBinding = physicsBindings.get(opening);
914
+ // A channel the component's OWN `useFrame` drives through the body's api is
915
+ // NOT forwarded in any sense a write can rely on: the hook may read the
916
+ // callsite literal at spawn, but the per-frame driver has the last word and
917
+ // the authored value is dead on the next tick (racing-game's `Train` — a
918
+ // Kinematic body re-posed from its animated group every frame; the doctor's
919
+ // settle leg measured the authored spawn discarded, digit for digit). Those
920
+ // channels leave the forwarded set here, and the component is classified
921
+ // simulation-owned below, so a callsite write refuses with the sentence
922
+ // that names the real owner instead of persisting a literal that lies.
923
+ const apiDriven = (rootBinding?.apiDriven ?? []) as readonly string[];
924
+ const toBinding = ((rootBinding?.forwarded ?? []) as readonly string[]).filter(
925
+ (channel) => !apiDriven.includes(channel),
926
+ );
927
+ const declared = propForwarding(parameters[0], nativeRootTag, sf, typeAliases, builtinPropTypes);
928
+ // A GUARD WRAPPER spells its props AS the component it delegates to —
929
+ // `props: Parameters<typeof WeaponModelBody>[0]` — and spreads them whole.
930
+ // `typeAllowsStandardRootProps` reads type TEXT and aliases, so that spelling
931
+ // reads as "accepts no root props" and the wrapper was reported as forwarding
932
+ // nothing: first-person's `WeaponModel` (the hook-free discriminator check in
933
+ // front of `WeaponModelBody` that its own header documents, bought by runhuman
934
+ // pass 97) drew a permanent R3F002 telling the author to forward
935
+ // position/rotation/scale it already forwards, all three, through `{...props}`.
936
+ // The delegate's contract is resolved just above, so a wrapper whose props are
937
+ // SPELLED as the delegate's accepts exactly what the delegate accepts.
938
+ const forwarding =
939
+ !declared.standardRootProps &&
940
+ delegated &&
941
+ typeIsDelegateProps(parameters[0]?.type, rootTag, sf)
942
+ ? { ...declared, standardRootProps: true }
943
+ : declared;
944
+ const toTag = forwardedObjectProps(opening, forwarding, sf);
945
+ const ownForwarding = R3F_TRANSFORM_PROPS.filter(
946
+ (channel) => toTag.includes(channel) || toBinding.includes(channel),
947
+ );
948
+ const transformProps = delegated
949
+ ? ownForwarding.filter((channel) => delegated.transformProps.includes(channel))
950
+ : ownForwarding;
951
+ const forwardsVisible =
952
+ toTag.includes('visible') && (!delegated || delegated.visibleProp === true);
953
+
954
+ // A built-in package contract may only ADD authorability. A component that
955
+ // returns an external component while forwarding no transform channel to it
956
+ // is exactly as opaque as it was before the table existed — reading the
957
+ // package's root as its own would claim a native root the component never
958
+ // routes anything to, and turn a silent unknown into a "does not forward"
959
+ // warning about source the author cannot act on.
960
+ if (isBuiltinR3fContract(delegated) && transformProps.length === 0) {
961
+ return { root: 'unknown', transformProps: [] };
962
+ }
963
+
964
+ // WHICH of the forwarded channels a BODY reads, kept because the answer
965
+ // changes what an authored write has to survive (see `bodyForwarded`).
966
+ const bodyForwarded = transformProps.filter((channel) => toBinding.includes(channel));
967
+
968
+ return {
969
+ root: 'single',
970
+ rootTag: nativeRootTag,
971
+ transformProps,
972
+ ...(bodyForwarded.length > 0 ? { bodyForwarded } : {}),
973
+ ...(forwardsVisible ? { visibleProp: true as const } : {}),
974
+ ...simulationOwnership(opening, transformProps, motionRefs),
975
+ // The api-driven case (see `apiDriven` above): the component drives its
976
+ // body every frame, so its instances are placed by the simulation exactly
977
+ // as `runtimeMotionRefs`'s direct-ref case is.
978
+ ...(apiDriven.length > 0 ? { simulationOwnedTransform: true as const } : {}),
979
+ };
980
+ }
981
+
982
+ interface R3fComponentDefinition {
983
+ name: string;
984
+ parameters: ts.NodeArray<ts.ParameterDeclaration>;
985
+ body: ts.ConciseBody;
986
+ }
987
+
988
+ function componentDefinitions(statement: ts.Statement): R3fComponentDefinition[] {
989
+ if (ts.isFunctionDeclaration(statement) && statement.name && statement.body) {
990
+ return /^[A-Z]/.test(statement.name.text)
991
+ ? [{ name: statement.name.text, parameters: statement.parameters, body: statement.body }]
992
+ : [];
993
+ }
994
+ if (!ts.isVariableStatement(statement)) return [];
995
+ return statement.declarationList.declarations.flatMap((declaration) => {
996
+ if (
997
+ !ts.isIdentifier(declaration.name) ||
998
+ !/^[A-Z]/.test(declaration.name.text) ||
999
+ !declaration.initializer ||
1000
+ (!ts.isArrowFunction(declaration.initializer) &&
1001
+ !ts.isFunctionExpression(declaration.initializer))
1002
+ ) {
1003
+ return [];
1004
+ }
1005
+ return [
1006
+ {
1007
+ name: declaration.name.text,
1008
+ parameters: declaration.initializer.parameters,
1009
+ body: declaration.initializer.body,
1010
+ },
1011
+ ];
1012
+ });
1013
+ }
1014
+
1015
+ function collectR3fComponentContracts(
1016
+ sf: ts.SourceFile,
1017
+ importedContracts: ReadonlyMap<string, R3fComponentContract> = new Map(),
1018
+ importedTypeAliases: ReadonlyMap<string, R3fTypeAlias> = new Map(),
1019
+ ): Map<string, R3fComponentContract> {
1020
+ // Imported first, so a local declaration of the same name shadows it exactly
1021
+ // as JavaScript lexical scope does — the same rule `importedContracts`
1022
+ // already follows for components.
1023
+ const aliases = new Map<string, R3fTypeAlias>(importedTypeAliases);
1024
+ for (const statement of sf.statements) {
1025
+ if (ts.isTypeAliasDeclaration(statement))
1026
+ aliases.set(statement.name.text, { type: statement.type, sf });
1027
+ }
1028
+ const builtins = builtinRootPropTypes(sf);
1029
+
1030
+ const definitions = sf.statements.flatMap(componentDefinitions);
1031
+ const motionRefs = runtimeMotionRefs(sf, sf);
1032
+ const physicsBindings = physicsBindingsByElement(sf);
1033
+ const contracts = new Map(importedContracts);
1034
+ // Components commonly delegate their native root to another local component.
1035
+ // Resolve that ordinary React composition to a fixed point rather than
1036
+ // requiring definition order or an authoring-specific wrapper.
1037
+ for (let pass = 0; pass <= definitions.length; pass += 1) {
1038
+ let changed = false;
1039
+ for (const definition of definitions) {
1040
+ const next = componentContract(
1041
+ definition.parameters,
1042
+ definition.body,
1043
+ sf,
1044
+ aliases,
1045
+ builtins,
1046
+ contracts,
1047
+ motionRefs,
1048
+ physicsBindings,
1049
+ );
1050
+ const previous = contracts.get(definition.name);
1051
+ if (JSON.stringify(previous) !== JSON.stringify(next)) {
1052
+ contracts.set(definition.name, next);
1053
+ changed = true;
1054
+ }
1055
+ }
1056
+ if (!changed) break;
1057
+ }
1058
+ return contracts;
1059
+ }
1060
+
1061
+ /** Analyze the component bindings visible in one R3F module. Imported
1062
+ * bindings are supplied by the project resolver; local definitions override
1063
+ * them exactly as JavaScript lexical scope does. */
1064
+ export function analyzeR3fComponentContracts(
1065
+ code: string,
1066
+ file: string,
1067
+ importedContracts: ReadonlyMap<string, R3fComponentContract> = new Map(),
1068
+ importedTypeAliases: ReadonlyMap<string, R3fTypeAlias> = new Map(),
1069
+ ): Map<string, R3fComponentContract> {
1070
+ const sf = parseAuthoringTsx(file, code);
1071
+ return collectR3fComponentContracts(sf, importedContracts, importedTypeAliases);
1072
+ }
1073
+
1074
+ /**
1075
+ * Mirrors `@volter/editor-project/adapter/adapter-surface`'s `AdapterSurface` vocabulary
1076
+ * (`'three' | 'canvas' | 'dom'`) as a bare literal union rather than
1077
+ * importing it: this module is deliberately dependency-free (see the module
1078
+ * doc comment above `OidEntry`) so a page with no bundler alias resolution can
1079
+ * import it too. Every caller
1080
+ * that already carries the real `AdapterSurface` (e.g. `binding-resolver.ts`)
1081
+ * may pass it here directly; the two types are structurally identical.
1082
+ */
1083
+ export type DeclaredRootSurface = 'three' | 'canvas' | 'dom';
1084
+
1085
+ /**
1086
+ * Intrinsic (lowercase-initial) JSX tags that exist ONLY in react-three-fiber's
1087
+ * reconciler — there is no HTML or SVG element with any of these names.
1088
+ *
1089
+ * DIAGNOSTIC EVIDENCE ONLY. This does not classify a file's dialect and must
1090
+ * not be made to: which region a file belongs to is decided by declaration and
1091
+ * import reach (`file-region-resolver.ts`), because that is a fact the game's
1092
+ * author states or the host EXECUTES, while a tag list is a guess about a file
1093
+ * whose imports it cannot see. What the scan is still for is telling an author
1094
+ * what the editor OBSERVED: the `OID003`/`OID004` messages in
1095
+ * `server/project-root-surface.ts` name the tags a file renders, and `OID004` —
1096
+ * a file rendering both dialects' intrinsics, which one attribute cannot serve
1097
+ * — is nothing BUT this observation.
1098
+ *
1099
+ * Deliberately CONSERVATIVE: every entry is checked against the HTML and SVG
1100
+ * element sets, so nothing here can be a DOM tag. `line`, `points`, `sprite`,
1101
+ * `audio`, `color` and `fog` are all real R3F intrinsics but are EXCLUDED from
1102
+ * this list where SVG/HTML also spells them (`<line>`, `<audio>`) — the list is
1103
+ * evidence, not a catalogue, and a wrong entry would flip a genuine DOM file.
1104
+ * `[a-z][A-Za-z]*(Geometry|Material)` covers the whole geometry/material family
1105
+ * (`boxGeometry`, `meshStandardMaterial`, `shaderMaterial`, …) without naming
1106
+ * each one; the leading lowercase class is what keeps it from matching a
1107
+ * CUSTOM component (`<MyMaterial>`), which is a callsite, not an intrinsic.
1108
+ */
1109
+ const R3F_ONLY_INTRINSIC_RE =
1110
+ /<(mesh|instancedMesh|batchedMesh|skinnedMesh|group|primitive|lineSegments|bufferGeometry|bufferAttribute|instancedBufferAttribute|[a-z][A-Za-z]*Geometry|[a-z][A-Za-z]*Material|ambientLight|directionalLight|pointLight|spotLight|hemisphereLight|rectAreaLight|perspectiveCamera|orthographicCamera|axesHelper|gridHelper|boxHelper)[\s/>]/g;
1111
+
1112
+ /**
1113
+ * The mirror image: intrinsic tags that exist ONLY in the react-dom
1114
+ * reconciler.
1115
+ *
1116
+ * DIAGNOSTIC EVIDENCE ONLY, on the same terms as the R3F list above: it
1117
+ * classifies no file. What a file renders is reported (`OID003`/`OID004` in
1118
+ * `server/project-root-surface.ts`); which region a file BELONGS to is a
1119
+ * declaration or a reach, never a property of its bytes. Excludes every name R3F also spells as an intrinsic
1120
+ * (`line`, `points`, `sprite`, `audio`, `color`, `fog`, `path`, `circle`), so
1121
+ * a hit here is unambiguous.
1122
+ */
1123
+ const DOM_ONLY_INTRINSIC_RE =
1124
+ /<(div|span|p|button|section|header|footer|nav|main|aside|form|input|textarea|select|option|label|table|thead|tbody|tr|td|th|img|iframe|br|hr|strong|em|pre|code|canvas|ul|ol|li|h[1-6])[\s/>]/g;
1125
+
1126
+ /**
1127
+ * What ONE file's own bytes say about which reconciler its JSX targets.
1128
+ *
1129
+ * DIAGNOSTIC EVIDENCE ONLY — it decides no attribute anywhere. What a file
1130
+ * renders is REPORTED (`server/project-root-surface.ts`'s `OID003`/`OID004`);
1131
+ * which region it belongs to is a declaration or a reach, never a property of
1132
+ * its bytes.
1133
+ *
1134
+ * Deliberately a cheap regex scan, not an AST walk, because it runs in a
1135
+ * `transform` hook on every project `.tsx`. COMMENTS are
1136
+ * stripped first (see {@link stripCommentsForScan}) so prose that merely
1137
+ * NAMES a tag cannot count as rendering it; a mention inside a string still
1138
+ * matches, which is deliberate — import specifiers ARE strings.
1139
+ */
1140
+ export interface SourceDialectEvidence {
1141
+ /** Imports the R3F reconciler, or the upstreamed `@vgai/game-runtime/world3d-react` bridge. */
1142
+ reconcilerImport: boolean;
1143
+ /** Distinct R3F-only intrinsic tags this file renders. */
1144
+ r3fOnlyTags: string[];
1145
+ /** Distinct DOM-only intrinsic tags this file renders. */
1146
+ domOnlyTags: string[];
1147
+ }
1148
+
1149
+ function distinctTags(code: string, pattern: RegExp): string[] {
1150
+ const found = new Set<string>();
1151
+ for (const match of code.matchAll(pattern)) if (match[1]) found.add(match[1]);
1152
+ return [...found];
1153
+ }
1154
+
1155
+ /**
1156
+ * Drop line and block comments before the dialect scan, copying string
1157
+ * literals through untouched. The shipped case that bought this: the starter
1158
+ * dev-tools JSDoc names its command shape `cheat.<group>.<label>` — `group`
1159
+ * is an R3F-only intrinsic and `label` a DOM-only one, so ONE comment line
1160
+ * made a fresh scaffold boot with a dialect-mixing diagnostic about a file
1161
+ * with no JSX in it. A diagnostic channel agents are told to trust cannot
1162
+ * cry wolf over prose.
1163
+ *
1164
+ * Single pass, no AST — the transform-hook perf bar stands. Known residual
1165
+ * imprecision, accepted: a regex literal containing `//` or `/*` is misread
1166
+ * as a comment and drops the rest of itself — which degrades toward "no
1167
+ * evidence" (the safe default), never toward a fabricated tag.
1168
+ */
1169
+ function stripCommentsForScan(code: string): string {
1170
+ const kept: string[] = [];
1171
+ let i = 0;
1172
+ while (i < code.length) {
1173
+ const ch = code[i]!;
1174
+ const next = code[i + 1];
1175
+ if (ch === '/' && (next === '/' || next === '*')) {
1176
+ // A space, not nothing: gluing a block comment's neighbors could mint
1177
+ // a token that was never in the source.
1178
+ if (next === '*') kept.push(' ');
1179
+ i = commentEnd(code, i, next);
1180
+ } else if (ch === "'" || ch === '"' || ch === '`') {
1181
+ const end = stringLiteralEnd(code, i);
1182
+ kept.push(code.slice(i, end));
1183
+ i = end;
1184
+ } else {
1185
+ kept.push(ch);
1186
+ i += 1;
1187
+ }
1188
+ }
1189
+ return kept.join('');
1190
+ }
1191
+
1192
+ /** End index (exclusive) of the comment opening at `start` — a `//` comment
1193
+ * stops BEFORE its newline (the newline stays in the scanned text). */
1194
+ function commentEnd(code: string, start: number, kind: '/' | '*'): number {
1195
+ if (kind === '/') {
1196
+ const newline = code.indexOf('\n', start + 2);
1197
+ return newline === -1 ? code.length : newline;
1198
+ }
1199
+ const close = code.indexOf('*/', start + 2);
1200
+ return close === -1 ? code.length : close + 2;
1201
+ }
1202
+
1203
+ /** End index (exclusive) of the string literal opening at `start`, honoring
1204
+ * backslash escapes; an unterminated literal runs to the end of the file. */
1205
+ function stringLiteralEnd(code: string, start: number): number {
1206
+ const quote = code[start]!;
1207
+ let i = start + 1;
1208
+ while (i < code.length) {
1209
+ const ch = code[i]!;
1210
+ if (ch === '\\') i += 2;
1211
+ else if (ch === quote) return i + 1;
1212
+ else i += 1;
1213
+ }
1214
+ return code.length;
1215
+ }
1216
+
1217
+ /** Scan one file's source for self-contained evidence of its JSX dialect. */
1218
+ export function sourceDialectEvidence(code: string): SourceDialectEvidence {
1219
+ const scanned = stripCommentsForScan(code);
1220
+ return {
1221
+ reconcilerImport:
1222
+ scanned.includes('@react-three/fiber') ||
1223
+ scanned.includes('@vgai/game-runtime/world3d-react'),
1224
+ r3fOnlyTags: distinctTags(scanned, R3F_ONLY_INTRINSIC_RE),
1225
+ domOnlyTags: distinctTags(scanned, DOM_ONLY_INTRINSIC_RE),
1226
+ };
1227
+ }
1228
+
1229
+ /** True when a file's own bytes prove it authors for the R3F reconciler. */
1230
+ export function sourceProvesR3f(evidence: SourceDialectEvidence): boolean {
1231
+ return evidence.reconcilerImport || evidence.r3fOnlyTags.length > 0;
1232
+ }
1233
+
1234
+ /**
1235
+ * THE SOURCE-ID ATTRIBUTE for one file, decided by its REGION and nothing else.
1236
+ *
1237
+ * 1. **The region (authoritative).** `surface` is the answer
1238
+ * `file-region-resolver.ts` gave for THIS file — a declaration the game
1239
+ * made (a region `include` glob, a region `mounts` entry, or the manifest
1240
+ * root whose `entry` this file is) or the region whose import closure
1241
+ * reaches it. `'three'` stamps `userData-oid`; `'canvas'`/`'dom'` stamp
1242
+ * `data-oid`. It is resolved from OUTSIDE the file, exactly where the fact
1243
+ * lives, which is what makes it right for a world whose entry
1244
+ * default-exports a component and imports NEITHER `@react-three/fiber` nor
1245
+ * `@vgai/game-runtime/world3d-react` (legal: R3F's global JSX-intrinsics augmentation,
1246
+ * or an entry composed entirely of already-typed child components, needs no
1247
+ * import in THIS file). Every call site threads it from a context that
1248
+ * resolved it per file: `binding-resolver.ts`'s per-surface resolvers, the
1249
+ * browser bundle's own per-file resolution
1250
+ * (`browser-transpile.ts`), and the dev server's
1251
+ * `server/project-root-surface.ts` — shared with `classifyProjectHotUpdate`,
1252
+ * the HMR half of this same decision, so the two cannot drift apart by hand.
1253
+ *
1254
+ * 2. **The DEFAULT.** When `surface` is `undefined` — no region placed the
1255
+ * file (an ad-hoc file with no `vgai.project.json` above it, or a module no
1256
+ * region's import closure reaches and none declares) — the answer is
1257
+ * `data-oid`, the documented default, and the CALLER says so out loud
1258
+ * (`server/project-root-surface.ts`'s `OID001`/`OID002`). There is
1259
+ * deliberately no source-text rung: a probe over a file's own bytes answered
1260
+ * for a file it could not place, silently, and being wrong that way is
1261
+ * indistinguishable from being right. Zero inference (ARCHITECTURE-CORE
1262
+ * §The editor protocol) makes the fix a declaration — an `include` glob or
1263
+ * a `mounts` entry in `vgai.adapter.ts` — not a better heuristic.
1264
+ *
1265
+ * Nothing about a file's own bytes outranks the region — not even a file that
1266
+ * renders THREE host elements while its region says `dom`. That disagreement is
1267
+ * FATAL rather than noisy (`data-oid` on a THREE object pierces inside fiber,
1268
+ * writes `object.data`, and throws on the next apply), which is exactly why it
1269
+ * is reported at full volume as declared-vs-measured DRIFT
1270
+ * (`server/project-root-surface.ts`'s `OID003`) and repaired by fixing the
1271
+ * declaration. A stamp derived from the bytes instead would make the
1272
+ * declaration untestable, which is how a wrong one stays invisible.
1273
+ *
1274
+ * Lives HERE, beside the transform it parameterizes, rather than in
1275
+ * `vite-plugin-ui-oid.ts` (which re-exports it for its existing callers):
1276
+ * a caller in a page reaches the same `transformSource`, and that path cannot
1277
+ * import a `node:fs`-bearing module.
1278
+ */
1279
+ export function oidAttributeForSurface(
1280
+ declaredSurface?: DeclaredRootSurface,
1281
+ ): 'data-oid' | 'userData-oid' {
1282
+ return declaredSurface === 'three' ? 'userData-oid' : 'data-oid';
1283
+ }
1284
+
1285
+ /** Options for {@link transformSource}. */
1286
+ export interface TransformSourceOptions {
1287
+ /**
1288
+ * The attribute name stamped on every JSX element (default `data-oid`).
1289
+ * R3F-dialect files (spike W1) pass `userData-oid` instead:
1290
+ * react-three-fiber's dashed-prop piercing writes `object.userData.oid`
1291
+ * on the constructed Object3D at reconcile time — zero runtime cost, and
1292
+ * the editor's R3F authoring adapter reads it back off the live scene
1293
+ * graph. The OID→{file,line,col} index is attribute-agnostic (keyed by
1294
+ * oid only), so every `/__ui-source/*` endpoint works unchanged for
1295
+ * either dialect.
1296
+ */
1297
+ attribute?: string;
1298
+ /** Contracts for ordinary project-local imported component bindings. */
1299
+ importedR3fContracts?: ReadonlyMap<string, R3fComponentContract>;
1300
+ /** The prop-shape type aliases those same imports bring into scope. A
1301
+ * project that writes its canonical prop shape once and imports it is
1302
+ * otherwise read as forwarding nothing. */
1303
+ importedR3fTypeAliases?: ReadonlyMap<string, R3fTypeAlias>;
1304
+ /** The file is mounted by @pixi/react. Local component callsites propagate
1305
+ * their OID onto the component's first native Pixi host, exactly as R3F
1306
+ * does with userData, but through ordinary data-* constructor fields. */
1307
+ canvasComponents?: boolean;
1308
+ }
1309
+
1310
+ /**
1311
+ * The local names bound to drei's `<Html>` in this file — the ONE seam where a
1312
+ * three-surface file renders through **react-dom** instead of the R3F
1313
+ * reconciler. `<Html>` creates a real `<div>`, mounts a `createRoot` on it and
1314
+ * renders its children there, so every host element inside it is a DOM element
1315
+ * and the R3F-pierced identity props this transform stamps on host elements
1316
+ * (`userData-oid`, `userData-authoringInstance`, `userData-authoringLabel`,
1317
+ * and the `userData={{}}` seed) have nowhere to land.
1318
+ *
1319
+ * Measured: the starter template's own `BotHud` — screen-pinned dev chrome
1320
+ * built on `<Html>`, the documented way to put DOM in a Three world — logged
1321
+ * FOUR `React does not recognize the \`userData-…\` prop on a DOM element`
1322
+ * errors on every bot run, one per stamped prop. Inside an `<Html>` subtree we
1323
+ * therefore stamp `data-oid`, byte-for-byte what a `dom` root gets.
1324
+ *
1325
+ * Lexical by design: this reads the subtree written in THIS file, which is the
1326
+ * whole extent the transform can see. A component defined elsewhere and
1327
+ * rendered inside `<Html>` is transformed by its own pass against its own
1328
+ * declared surface.
1329
+ */
1330
+ function dreiHtmlLocalNames(sf: ts.SourceFile): Set<string> {
1331
+ const names = new Set<string>();
1332
+ for (const statement of sf.statements) {
1333
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
1334
+ continue;
1335
+ }
1336
+ const specifier = statement.moduleSpecifier.text;
1337
+ if (specifier !== '@react-three/drei' && !specifier.startsWith('@react-three/drei/')) continue;
1338
+ const bindings = statement.importClause?.namedBindings;
1339
+ if (!bindings || !ts.isNamedImports(bindings)) continue;
1340
+ for (const element of bindings.elements) {
1341
+ if ((element.propertyName?.text ?? element.name.text) === 'Html')
1342
+ names.add(element.name.text);
1343
+ }
1344
+ }
1345
+ return names;
1346
+ }
1347
+
1348
+ /**
1349
+ * Local JSX names bound by an import from an INSTALLED PACKAGE — every
1350
+ * specifier that is not relative (`./x`, `../x`), which is exactly the line
1351
+ * `r3f-project-contracts.ts`'s `resolveRelativeModule` already draws between
1352
+ * "source this editor walks" and "a dependency it never guesses about".
1353
+ *
1354
+ * Why the callsite stamp must skip them: `__vgaiOid`/`__vgaiLabel` are a
1355
+ * CONTRACT BETWEEN TWO HALVES OF THIS TRANSFORM. The callsite half writes
1356
+ * them; the only thing that ever reads them is the callee half —
1357
+ * `componentInstanceExpressions` destructuring them out of the component's own
1358
+ * parameters and re-emitting them as `userData-authoringInstance` on that
1359
+ * component's host elements. Nothing anywhere reads them off a React instance
1360
+ * or a DOM node. A package module is never served through this transform, so
1361
+ * the callee half never runs for it: the props have no reader by construction,
1362
+ * and the only thing they can still do is escape.
1363
+ *
1364
+ * And they do. Measured on the vendored `racing-game` ingest: `App.tsx` is a
1365
+ * MIXED file (an R3F `Scene` and a DOM `Hud` in one module, so the file's one
1366
+ * dialect is `userData-oid`), and its `<Canvas>` — `@react-three/fiber`'s own,
1367
+ * which spreads its rest props onto the `<div>` it wraps the canvas in — put
1368
+ * `__vgaiOid` and `__vgaiLabel` straight into the DOM, for two permanent
1369
+ * `React does not recognize the \`__vgaiOid\` prop on a DOM element` errors on
1370
+ * every mount of a real game. Nothing was lost by not stamping it, because
1371
+ * nothing was reading it.
1372
+ *
1373
+ * The one package component that DOES carry editor identity earns it through
1374
+ * the hand-audited contract table instead (`BUILTIN_R3F_CONTRACTS`), which
1375
+ * describes the native root the package forwards to — a claim about a specific
1376
+ * package's public API, never a guess made at a callsite.
1377
+ */
1378
+ function packageImportedLocalNames(sf: ts.SourceFile): Set<string> {
1379
+ const names = new Set<string>();
1380
+ for (const statement of sf.statements) {
1381
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
1382
+ continue;
1383
+ }
1384
+ if (statement.moduleSpecifier.text.startsWith('.')) continue;
1385
+ const clause = statement.importClause;
1386
+ if (!clause) continue;
1387
+ if (clause.name) names.add(clause.name.text);
1388
+ const bindings = clause.namedBindings;
1389
+ if (!bindings) continue;
1390
+ if (ts.isNamespaceImport(bindings)) {
1391
+ names.add(bindings.name.text);
1392
+ continue;
1393
+ }
1394
+ for (const element of bindings.elements) names.add(element.name.text);
1395
+ }
1396
+ return names;
1397
+ }
1398
+
1399
+ /** Drei cameras forward these props to their native Three camera. */
1400
+ function dreiCameraLocalNames(sf: ts.SourceFile): Set<string> {
1401
+ const names = new Set<string>();
1402
+ const cameras = ['PerspectiveCamera', 'OrthographicCamera'];
1403
+ for (const statement of sf.statements) {
1404
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier))
1405
+ continue;
1406
+ const specifier = statement.moduleSpecifier.text;
1407
+ if (
1408
+ specifier !== '@react-three/drei' &&
1409
+ !cameras.some((name) => specifier === `@react-three/drei/core/${name}`)
1410
+ )
1411
+ continue;
1412
+ const bindings = statement.importClause?.namedBindings;
1413
+ if (bindings && ts.isNamespaceImport(bindings)) {
1414
+ for (const camera of cameras) names.add(`${bindings.name.text}.${camera}`);
1415
+ } else if (bindings && ts.isNamedImports(bindings)) {
1416
+ for (const element of bindings.elements) {
1417
+ if (cameras.includes(element.propertyName?.text ?? element.name.text))
1418
+ names.add(element.name.text);
1419
+ }
1420
+ }
1421
+ }
1422
+ return names;
1423
+ }
1424
+
1425
+ /**
1426
+ * Component-shaped JSX names bound by a function PARAMETER — the second way a
1427
+ * tag's callee is unknowable at transform time, and the same hazard as
1428
+ * {@link packageImportedLocalNames} for exactly the same reason.
1429
+ *
1430
+ * `__vgaiOid`/`__vgaiLabel` are a contract between two halves of this
1431
+ * transform: the callsite half writes them, the callee half destructures them
1432
+ * out of the component's own parameters. A tag bound to a parameter names
1433
+ * WHATEVER the caller passed, so this module cannot know whether the callee
1434
+ * half ever ran for it — and where it did not, the props' only remaining
1435
+ * behaviour is to escape into the DOM through any rest-prop spread.
1436
+ *
1437
+ * MEASURED on the vendored `racing-game` ingest, against the SERVED modules:
1438
+ * `useToggle.tsx`'s `<ToggledComponent {...props} />` (its `ToggledComponent`
1439
+ * is a parameter — drei's `Stats`/`OrbitControls`, cannon's `Debug`, or a
1440
+ * project component, depending on the call) and every R3F `*.stories.tsx`
1441
+ * decorator's `<Story />`. Two standing
1442
+ * `React does not recognize the \`__vgaiOid\` prop on a DOM element` errors on
1443
+ * every run of the acceptance portfolio.
1444
+ *
1445
+ * Nothing is lost by not stamping, and identity actually IMPROVES: a forwarder
1446
+ * spreads the caller's own props first, so the oid the REAL callsite wrote
1447
+ * (`<ToggledMap/>` in `App.tsx`) now reaches the wrapped component instead of
1448
+ * being overwritten by a literal naming the forwarder's own line.
1449
+ *
1450
+ * Scope is the whole file rather than lexical scope on purpose: a name that is
1451
+ * a parameter ANYWHERE in a module is not a stable component identity to stamp
1452
+ * against, and a per-scope walk would buy precision this decision does not use.
1453
+ */
1454
+ function parameterBoundLocalNames(sf: ts.SourceFile): Set<string> {
1455
+ const names = new Set<string>();
1456
+ const addBinding = (name: ts.BindingName): void => {
1457
+ if (ts.isIdentifier(name)) {
1458
+ if (/^[A-Z]/.test(name.text)) names.add(name.text);
1459
+ return;
1460
+ }
1461
+ for (const element of name.elements) {
1462
+ if (ts.isBindingElement(element)) addBinding(element.name);
1463
+ }
1464
+ };
1465
+ const visit = (node: ts.Node): void => {
1466
+ if (ts.isFunctionLike(node)) {
1467
+ for (const parameter of node.parameters) addBinding(parameter.name);
1468
+ }
1469
+ ts.forEachChild(node, visit);
1470
+ };
1471
+ visit(sf);
1472
+ return names;
1473
+ }
1474
+
1475
+ /** JSX spellings that resolve to React's transparent Fragment. A Fragment is
1476
+ * composition, not a host node: it accepts only key/children, has no runtime
1477
+ * element to select, and must never receive an OID prop. */
1478
+ function reactFragmentNames(sf: ts.SourceFile): Set<string> {
1479
+ const names = new Set(['Fragment', 'React.Fragment']);
1480
+ for (const statement of sf.statements) {
1481
+ if (
1482
+ !ts.isImportDeclaration(statement) ||
1483
+ !ts.isStringLiteral(statement.moduleSpecifier) ||
1484
+ statement.moduleSpecifier.text !== 'react'
1485
+ ) {
1486
+ continue;
1487
+ }
1488
+ const clause = statement.importClause;
1489
+ if (clause?.name) names.add(`${clause.name.text}.Fragment`);
1490
+ const bindings = clause?.namedBindings;
1491
+ if (bindings && ts.isNamespaceImport(bindings)) {
1492
+ names.add(`${bindings.name.text}.Fragment`);
1493
+ } else if (bindings && ts.isNamedImports(bindings)) {
1494
+ for (const element of bindings.elements) {
1495
+ if ((element.propertyName?.text ?? element.name.text) === 'Fragment') {
1496
+ names.add(element.name.text);
1497
+ }
1498
+ }
1499
+ }
1500
+ }
1501
+ return names;
1502
+ }
1503
+
1504
+ /**
1505
+ * Transform source: insert `data-oid="…"` (or `opts.attribute="…"`) on every
1506
+ * JSX element and register the index. In the R3F dialect, a custom component
1507
+ * callsite receives editor-only props whose values are propagated onto the
1508
+ * component's existing host Object3Ds. Every transformed project component,
1509
+ * on every surface, consumes those props at its parameter boundary so a
1510
+ * cross-surface component or ordinary `{...props}` spread cannot forward them
1511
+ * into game DOM. That preserves per-instance source identity without inserting
1512
+ * a wrapper into the native tree. Returns the transformed code + index entries.
1513
+ */
1514
+ export function transformSource(
1515
+ code: string,
1516
+ file: string,
1517
+ store: OidStore,
1518
+ opts?: TransformSourceOptions,
1519
+ ): { code: string; entries: OidEntry[] } {
1520
+ const sf = parseAuthoringTsx(file, code);
1521
+ store.clearFile(file);
1522
+ const attribute = opts?.attribute ?? 'data-oid';
1523
+ const projectsComponentInstances =
1524
+ attribute === 'userData-oid' || opts?.canvasComponents === true;
1525
+ const sourceEdits: SourceEdit[] = [];
1526
+ const entries: OidEntry[] = [];
1527
+ const r3fComponentContracts =
1528
+ attribute === 'userData-oid'
1529
+ ? collectR3fComponentContracts(sf, opts?.importedR3fContracts, opts?.importedR3fTypeAliases)
1530
+ : new Map();
1531
+ const physicsBindings =
1532
+ attribute === 'userData-oid'
1533
+ ? physicsBindingsByElement(sf)
1534
+ : new Map<ts.JsxOpeningLikeElement, R3fPhysicsBinding>();
1535
+ const jointBindings =
1536
+ attribute === 'userData-oid'
1537
+ ? jointBindingsByElement(sf)
1538
+ : new Map<ts.JsxOpeningLikeElement, readonly R3fJointBinding[]>();
1539
+ const particleBindings =
1540
+ attribute === 'userData-oid'
1541
+ ? particleBindingsByElement(sf)
1542
+ : new Map<ts.JsxOpeningLikeElement, R3fParticleBinding>();
1543
+ const lodBindings =
1544
+ attribute === 'userData-oid'
1545
+ ? lodBindingsByElement(sf)
1546
+ : new Map<ts.JsxOpeningLikeElement, R3fLodBinding>();
1547
+ const environmentBindings =
1548
+ attribute === 'userData-oid'
1549
+ ? environmentBindingsByElement(sf)
1550
+ : new Map<ts.JsxOpeningLikeElement, R3fEnvironmentBinding>();
1551
+ const componentStack: Array<{
1552
+ name: string;
1553
+ instanceExpression: string;
1554
+ labelExpression: string;
1555
+ }> = [];
1556
+ // per (component:tag) occurrence counters for stable signatures
1557
+ const occ = new Map<string, number>();
1558
+
1559
+ const componentInstanceExpressions = (
1560
+ parameters: ts.NodeArray<ts.ParameterDeclaration>,
1561
+ ): { instanceExpression: string; labelExpression: string } => {
1562
+ const first = parameters[0];
1563
+ if (!first) {
1564
+ // Default the whole binding. Isolation/preview/bake call a stamped
1565
+ // export as a factory (`MainScene()` / `WOOD()`) with no args; a
1566
+ // required destructure of `undefined` is `Cannot destructure property
1567
+ // '__vgaiOid'`. React still passes `{}` at a real callsite.
1568
+ sourceEdits.push({ pos: parameters.pos, text: '{ __vgaiOid, __vgaiLabel } = {}' });
1569
+ return { instanceExpression: '__vgaiOid', labelExpression: '__vgaiLabel' };
1570
+ }
1571
+ if (ts.isObjectBindingPattern(first.name)) {
1572
+ sourceEdits.push({
1573
+ pos: first.name.getStart(sf) + 1,
1574
+ text: '__vgaiOid, __vgaiLabel, ',
1575
+ });
1576
+ // After the WHOLE parameter, including a type annotation:
1577
+ // `({ name, ...props }: T)` must become `({ __vgaiOid, …, name, ...props }: T = {})`.
1578
+ // Inserting at the binding's end produced `} = {}: T`, which is not
1579
+ // TypeScript and broke every typed scene export the look verb imports.
1580
+ if (!first.initializer) {
1581
+ sourceEdits.push({ pos: first.getEnd(), text: ' = {}' });
1582
+ }
1583
+ return { instanceExpression: '__vgaiOid', labelExpression: '__vgaiLabel' };
1584
+ }
1585
+ if (ts.isIdentifier(first.name) && !first.dotDotDotToken) {
1586
+ // Preserve the authored identifier as the REST binding. Code below the
1587
+ // parameter continues to read `props`, but it now sees only game props:
1588
+ // `function Card(props: Props)` becomes
1589
+ // `function Card({ __vgaiOid, __vgaiLabel, ...props }: Props)`.
1590
+ // Remove an optional marker while replacing the identifier; a binding
1591
+ // pattern cannot itself be optional, so its equivalent is a `{}` default.
1592
+ sourceEdits.push({
1593
+ pos: first.name.getStart(sf),
1594
+ end: first.questionToken?.end ?? first.name.getEnd(),
1595
+ text: `{ __vgaiOid, __vgaiLabel, ...${first.name.text} }`,
1596
+ });
1597
+ if (!first.initializer) {
1598
+ sourceEdits.push({ pos: first.getEnd(), text: ' = {}' });
1599
+ }
1600
+ return { instanceExpression: '__vgaiOid', labelExpression: '__vgaiLabel' };
1601
+ }
1602
+ return { instanceExpression: '__vgaiOid', labelExpression: '__vgaiLabel' };
1603
+ };
1604
+
1605
+ const pushComponent = (
1606
+ name: string | undefined,
1607
+ parameters: ts.NodeArray<ts.ParameterDeclaration> | undefined,
1608
+ ): boolean => {
1609
+ if (!name || !/^[A-Z]/.test(name) || !parameters) return false;
1610
+ // Component membership participates in the stable OID signature on every
1611
+ // JSX dialect. Three/Canvas callsites inject the transport props, while
1612
+ // every project component consumes them: a component declared on a DOM
1613
+ // mount can therefore be called safely from a Three-region module without
1614
+ // leaking editor metadata through its own rest props.
1615
+ componentStack.push({ name, ...componentInstanceExpressions(parameters) });
1616
+ return true;
1617
+ };
1618
+
1619
+ /** Local names of drei's `<Html>`, and how deep inside one we currently are —
1620
+ * see {@link dreiHtmlLocalNames}. Only meaningful in the R3F dialect; a
1621
+ * `data-oid` file already stamps what a DOM subtree wants. */
1622
+ const dreiHtmlNames = attribute === 'userData-oid' ? dreiHtmlLocalNames(sf) : new Set<string>();
1623
+ const dreiCameraNames =
1624
+ attribute === 'userData-oid' ? dreiCameraLocalNames(sf) : new Set<string>();
1625
+ /** Tags bound to an installed package — see {@link packageImportedLocalNames}.
1626
+ * Only meaningful in the R3F dialect: `data-oid` is a legal DOM attribute
1627
+ * and a package that spreads it renders it harmlessly. */
1628
+ const packageNames = projectsComponentInstances
1629
+ ? packageImportedLocalNames(sf)
1630
+ : new Set<string>();
1631
+ /** Tags bound to a function parameter — see {@link parameterBoundLocalNames}.
1632
+ * Same dialect gating and same reason as `packageNames`: only the transport
1633
+ * props can escape, and `data-oid` is a legal DOM attribute. */
1634
+ const parameterNames = projectsComponentInstances
1635
+ ? parameterBoundLocalNames(sf)
1636
+ : new Set<string>();
1637
+ const fragmentNames = reactFragmentNames(sf);
1638
+ let domSubtreeDepth = 0;
1639
+
1640
+ /** Oids of the JSX elements lexically enclosing the node being visited. */
1641
+ const jsxStack: string[] = [];
1642
+ /** Opening tag -> its oid, so the enclosing `JsxElement` can push it before
1643
+ * descending into the children that tag wraps. */
1644
+ const openedOid = new Map<ts.JsxOpeningElement, string>();
1645
+
1646
+ const visit = (node: ts.Node): void => {
1647
+ let pushed = false;
1648
+ if (ts.isFunctionDeclaration(node) && node.name)
1649
+ pushed = pushComponent(node.name.text, node.parameters);
1650
+ else if (
1651
+ ts.isVariableDeclaration(node) &&
1652
+ ts.isIdentifier(node.name) &&
1653
+ node.initializer &&
1654
+ (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))
1655
+ ) {
1656
+ pushed = pushComponent(node.name.text, node.initializer.parameters);
1657
+ }
1658
+
1659
+ if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
1660
+ const tag = node.tagName.getText(sf);
1661
+ if (fragmentNames.has(tag)) {
1662
+ ts.forEachChild(node, visit);
1663
+ if (pushed) componentStack.pop();
1664
+ return;
1665
+ }
1666
+ const componentContext = componentStack[componentStack.length - 1];
1667
+ const component = componentContext?.name ?? null;
1668
+ const sigBase = `${file}:${component}:${tag}`;
1669
+ const n = occ.get(sigBase) ?? 0;
1670
+ occ.set(sigBase, n + 1);
1671
+ const sig = `${sigBase}:${n}`;
1672
+ const oid = store.getOrCreate(sig);
1673
+ // Inside a drei `<Html>` the reconciler is react-dom, not fiber — see
1674
+ // `dreiHtmlLocalNames`. Everything below reads this, not `attribute`.
1675
+ const elementAttribute = domSubtreeDepth > 0 ? 'data-oid' : attribute;
1676
+ const { line, character } = sf.getLineAndCharacterOfPosition(node.getStart(sf));
1677
+ const localContract =
1678
+ elementAttribute === 'userData-oid' && /^[A-Z]/.test(tag)
1679
+ ? r3fComponentContracts.get(tag)
1680
+ : undefined;
1681
+ const entry: OidEntry = {
1682
+ file,
1683
+ line: line + 1,
1684
+ col: character,
1685
+ component,
1686
+ tag,
1687
+ authoredProps: authoredPropsOf(node, sf),
1688
+ ...(localContract ? { r3fAuthoring: localContract } : {}),
1689
+ ...(physicsBindings.has(node) ? { physicsBinding: physicsBindings.get(node)! } : {}),
1690
+ ...(jointBindings.has(node) ? { jointBindings: jointBindings.get(node)! } : {}),
1691
+ ...(particleBindings.has(node) ? { particleBinding: particleBindings.get(node)! } : {}),
1692
+ ...(lodBindings.has(node) ? { lodBinding: lodBindings.get(node)! } : {}),
1693
+ ...(environmentBindings.has(node)
1694
+ ? { environmentBinding: environmentBindings.get(node)! }
1695
+ : {}),
1696
+ ...(jsxStack.length > 0 ? { parentOid: jsxStack[jsxStack.length - 1]! } : {}),
1697
+ };
1698
+ store.index.set(oid, entry);
1699
+ entries.push(entry);
1700
+ if (ts.isJsxOpeningElement(node)) openedOid.set(node, oid);
1701
+ const isProjectCustomComponent =
1702
+ (elementAttribute === 'userData-oid' || opts?.canvasComponents === true) &&
1703
+ /^[A-Z]/.test(tag);
1704
+ if (isProjectCustomComponent) {
1705
+ const element = ts.isJsxOpeningElement(node) ? node.parent : node;
1706
+ if (ts.isJsxElement(element) || ts.isJsxSelfClosingElement(element)) {
1707
+ // An installed package's module never passes through this transform,
1708
+ // so the callee half of the `__vgaiOid` contract never runs for it and
1709
+ // the props can only escape (into the DOM, via any package component
1710
+ // that spreads its rest props onto a host element). The callsite is
1711
+ // still INDEXED above — the source entry, its parent and its contract
1712
+ // are unchanged; only the unreadable runtime prop is withheld. See
1713
+ // {@link packageImportedLocalNames}.
1714
+ if (packageNames.has(tag.split('.')[0] ?? tag)) {
1715
+ // Detailed and the two Drei cameras spread their remaining props
1716
+ // onto a native Three host, where the ordinary R3F OID belongs.
1717
+ if (lodBindings.has(node) || dreiCameraNames.has(tag)) {
1718
+ sourceEdits.push({ pos: node.attributes.end, text: ` userData-oid="${oid}"` });
1719
+ }
1720
+ ts.forEachChild(node, visit);
1721
+ if (pushed) componentStack.pop();
1722
+ return;
1723
+ }
1724
+ // A tag bound to a PARAMETER names whatever the caller passed, so the
1725
+ // callee half may never have run for it either — same withholding,
1726
+ // same reason, and the callsite stays indexed. See
1727
+ // {@link parameterBoundLocalNames}.
1728
+ if (parameterNames.has(tag.split('.')[0] ?? tag)) {
1729
+ ts.forEachChild(node, visit);
1730
+ if (pushed) componentStack.pop();
1731
+ return;
1732
+ }
1733
+ const authoredName = node.attributes.properties.find(
1734
+ (property): property is ts.JsxAttribute =>
1735
+ ts.isJsxAttribute(property) && property.name.getText(sf) === 'name',
1736
+ );
1737
+ const label =
1738
+ authoredName?.initializer && ts.isStringLiteral(authoredName.initializer)
1739
+ ? authoredName.initializer.text
1740
+ : tag;
1741
+ const labelAttribute =
1742
+ authoredName?.initializer && ts.isJsxExpression(authoredName.initializer)
1743
+ ? `__vgaiLabel={${authoredName.initializer.expression?.getText(sf) ?? JSON.stringify(tag)}}`
1744
+ : `__vgaiLabel=${JSON.stringify(label)}`;
1745
+ sourceEdits.push({
1746
+ pos: node.attributes.end,
1747
+ text: ` __vgaiOid="${oid}" ${labelAttribute}`,
1748
+ });
1749
+ }
1750
+ } else {
1751
+ // Host elements carry their identity directly.
1752
+ // Append after authored attributes. In R3F, an authored `userData={{…}}`
1753
+ // replaces the whole object at its position in prop order; putting the
1754
+ // pierced editor keys last prevents that prop from erasing identity.
1755
+ sourceEdits.push({ pos: node.attributes.end, text: ` ${elementAttribute}="${oid}"` });
1756
+ if (elementAttribute === 'userData-oid' && componentContext) {
1757
+ sourceEdits.push({
1758
+ pos: node.attributes.end,
1759
+ text:
1760
+ ` userData-authoringInstance={${componentContext.instanceExpression}}` +
1761
+ ` userData-authoringLabel={${componentContext.labelExpression}}` +
1762
+ ` userData-authoringComponent=${JSON.stringify(componentContext.name)}`,
1763
+ });
1764
+ } else if (opts?.canvasComponents === true && componentContext) {
1765
+ sourceEdits.push({
1766
+ pos: node.attributes.end,
1767
+ text:
1768
+ ` data-authoring-instance={${componentContext.instanceExpression}}` +
1769
+ ` data-authoring-label={${componentContext.labelExpression}}` +
1770
+ ` data-authoring-component=${JSON.stringify(componentContext.name)}`,
1771
+ });
1772
+ }
1773
+ if (
1774
+ elementAttribute === 'userData-oid' &&
1775
+ // NEVER on `<primitive>`. Its object is the GAME's, constructed before
1776
+ // React ever sees it, and `userData={{}}` REPLACES that object's own
1777
+ // `userData` wholesale — measured: a root carrying
1778
+ // `markComponentRoot(node, 'Stage')` from its constructor came out of
1779
+ // mount with nothing but the three editor keys, so every hierarchy
1780
+ // mark a class-owned root sets was destroyed by the editor's own
1781
+ // serve-time transform. An adopted object always HAS a `userData`
1782
+ // (three sets one on every Object3D/geometry/material), which is
1783
+ // precisely why the initializer below is unnecessary here and
1784
+ // destructive. That initializer's real subjects are the resources R3F
1785
+ // itself constructs.
1786
+ tag !== 'primitive' &&
1787
+ !node.attributes.properties.some(
1788
+ (property) => ts.isJsxAttribute(property) && property.name.getText(sf) === 'userData',
1789
+ )
1790
+ ) {
1791
+ // Some valid R3F host resources (for example BufferAttribute) do not
1792
+ // start with a `userData` object. Current Fiber deliberately refuses
1793
+ // to pierce `userData-*` through an undefined parent, so initialize
1794
+ // it before the editor metadata props are applied.
1795
+ sourceEdits.push({ pos: node.attributes.end, text: ' userData={{}}' });
1796
+ }
1797
+ }
1798
+ }
1799
+
1800
+ // A `<Tag>…</Tag>` pair encloses its children, but in the AST the opening
1801
+ // tag is their SIBLING — so walk it FIRST (which mints its oid), then push
1802
+ // that oid while descending into the children it wraps. Doing this in the
1803
+ // generic `forEachChild` tail would read the oid before it exists.
1804
+ if (ts.isJsxElement(node)) {
1805
+ visit(node.openingElement);
1806
+ const enclosingOid = openedOid.get(node.openingElement);
1807
+ if (enclosingOid !== undefined) jsxStack.push(enclosingOid);
1808
+ // `<Html>` ITSELF stays an R3F callsite (drei mounts a `<group>` for it);
1809
+ // only what it wraps is DOM.
1810
+ const opensDomSubtree = dreiHtmlNames.has(node.openingElement.tagName.getText(sf));
1811
+ if (opensDomSubtree) domSubtreeDepth++;
1812
+ for (const child of node.children) visit(child);
1813
+ if (opensDomSubtree) domSubtreeDepth--;
1814
+ if (enclosingOid !== undefined) jsxStack.pop();
1815
+ visit(node.closingElement);
1816
+ if (pushed) componentStack.pop();
1817
+ return;
1818
+ }
1819
+
1820
+ ts.forEachChild(node, visit);
1821
+ if (pushed) componentStack.pop();
1822
+ };
1823
+ visit(sf);
1824
+
1825
+ // Apply edits back-to-front so every offset stays relative to source bytes.
1826
+ sourceEdits.sort((a, b) => b.pos - a.pos);
1827
+ let out = code;
1828
+ for (const edit of sourceEdits) {
1829
+ out = out.slice(0, edit.pos) + edit.text + out.slice(edit.end ?? edit.pos);
1830
+ }
1831
+ return { code: out, entries };
1832
+ }
1833
+
1834
+ /** 1-based line + 0-based col -> absolute char offset in `code`. */
1835
+ export function lineColToOffset(code: string, line: number, col: number): number {
1836
+ let offset = 0;
1837
+ let cur = 1;
1838
+ while (cur < line) {
1839
+ const nl = code.indexOf('\n', offset);
1840
+ if (nl === -1) return code.length;
1841
+ offset = nl + 1;
1842
+ cur += 1;
1843
+ }
1844
+ return offset + col;
1845
+ }