@vgai/editor 0.2.0

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 (519) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +35 -0
  3. package/dist/a/b/c/test-nested.json +1 -0
  4. package/dist/assets/CCDIKSolver-Dz-ysxZF.js +1 -0
  5. package/dist/assets/SelectionBox-BXsv_6aH.js +1 -0
  6. package/dist/assets/browser-project-scripts-DOru4L1x.js +2 -0
  7. package/dist/assets/browserAll-CEkWYlFk.js +1 -0
  8. package/dist/assets/collision-world-DniMB0qq.glb +0 -0
  9. package/dist/assets/create-runtime-DYIWr3va.js +1 -0
  10. package/dist/assets/data-asset-Bb9sIM7i.js +5 -0
  11. package/dist/assets/esbuild-BHljloGq.wasm +0 -0
  12. package/dist/assets/fake-three-BMTQ9rl_.js +1 -0
  13. package/dist/assets/game-8SDZPPgH.js +1 -0
  14. package/dist/assets/game-BI111o5f.js +10 -0
  15. package/dist/assets/game-CevorxyV.js +1 -0
  16. package/dist/assets/game-Di1H0gFj.js +1 -0
  17. package/dist/assets/game-Di7XlywD.js +1 -0
  18. package/dist/assets/game-DjnCaw8R.js +1 -0
  19. package/dist/assets/game-Q9A0aXSV.js +1 -0
  20. package/dist/assets/game-pt_1lQLv.js +1 -0
  21. package/dist/assets/game-source-CvidN7AB.js +1 -0
  22. package/dist/assets/game-xsOho_wg.js +83 -0
  23. package/dist/assets/index-Bp5cecD6.js +12 -0
  24. package/dist/assets/index-CPVV6pjw.js +1 -0
  25. package/dist/assets/index-CpOnYyeD.js +213 -0
  26. package/dist/assets/index-Cs8xwv3e.js +1 -0
  27. package/dist/assets/index-DL7RcQ0n.js +27 -0
  28. package/dist/assets/index-Dr8H_6Rl.js +1 -0
  29. package/dist/assets/index-_bCaipUk.js +13650 -0
  30. package/dist/assets/ingest-siblings-Dz8eEqrV.js +1 -0
  31. package/dist/assets/mount-manifest-Bhk0ohsl.js +1 -0
  32. package/dist/assets/recast-navigation.wasm-compat-CvP4UGK6.js +1 -0
  33. package/dist/assets/scene-query-BivAHhJu.js +1 -0
  34. package/dist/assets/stats.module--VATS4Kh.js +1 -0
  35. package/dist/assets/webworkerAll-BLrH0F6F.js +1 -0
  36. package/dist/assets/xstate-animation-binding-1NGLmw6y.js +1 -0
  37. package/dist/assets/xstate.esm-BgUD33nl.js +3 -0
  38. package/dist/index.html +37 -0
  39. package/dist/ingest/racing-game/UPSTREAM.md +68 -0
  40. package/dist/ingest/racing-game/a/Inter-Black.woff +0 -0
  41. package/dist/ingest/racing-game/a/Inter-Black.woff2 +0 -0
  42. package/dist/ingest/racing-game/a/Inter-BlackItalic.woff +0 -0
  43. package/dist/ingest/racing-game/a/Inter-BlackItalic.woff2 +0 -0
  44. package/dist/ingest/racing-game/a/Inter-Bold.woff +0 -0
  45. package/dist/ingest/racing-game/a/Inter-Bold.woff2 +0 -0
  46. package/dist/ingest/racing-game/a/Inter-BoldItalic.woff +0 -0
  47. package/dist/ingest/racing-game/a/Inter-BoldItalic.woff2 +0 -0
  48. package/dist/ingest/racing-game/a/Inter-ExtraBold.woff +0 -0
  49. package/dist/ingest/racing-game/a/Inter-ExtraBold.woff2 +0 -0
  50. package/dist/ingest/racing-game/a/Inter-ExtraBoldItalic.woff +0 -0
  51. package/dist/ingest/racing-game/a/Inter-ExtraBoldItalic.woff2 +0 -0
  52. package/dist/ingest/racing-game/a/Inter-ExtraLight.woff +0 -0
  53. package/dist/ingest/racing-game/a/Inter-ExtraLight.woff2 +0 -0
  54. package/dist/ingest/racing-game/a/Inter-ExtraLightItalic.woff +0 -0
  55. package/dist/ingest/racing-game/a/Inter-ExtraLightItalic.woff2 +0 -0
  56. package/dist/ingest/racing-game/a/Inter-Italic.woff +0 -0
  57. package/dist/ingest/racing-game/a/Inter-Italic.woff2 +0 -0
  58. package/dist/ingest/racing-game/a/Inter-Light.woff +0 -0
  59. package/dist/ingest/racing-game/a/Inter-Light.woff2 +0 -0
  60. package/dist/ingest/racing-game/a/Inter-LightItalic.woff +0 -0
  61. package/dist/ingest/racing-game/a/Inter-LightItalic.woff2 +0 -0
  62. package/dist/ingest/racing-game/a/Inter-Medium.woff +0 -0
  63. package/dist/ingest/racing-game/a/Inter-Medium.woff2 +0 -0
  64. package/dist/ingest/racing-game/a/Inter-MediumItalic.woff +0 -0
  65. package/dist/ingest/racing-game/a/Inter-MediumItalic.woff2 +0 -0
  66. package/dist/ingest/racing-game/a/Inter-Regular.woff +0 -0
  67. package/dist/ingest/racing-game/a/Inter-Regular.woff2 +0 -0
  68. package/dist/ingest/racing-game/a/Inter-SemiBold.woff +0 -0
  69. package/dist/ingest/racing-game/a/Inter-SemiBold.woff2 +0 -0
  70. package/dist/ingest/racing-game/a/Inter-SemiBoldItalic.woff +0 -0
  71. package/dist/ingest/racing-game/a/Inter-SemiBoldItalic.woff2 +0 -0
  72. package/dist/ingest/racing-game/a/Inter-Thin.woff +0 -0
  73. package/dist/ingest/racing-game/a/Inter-Thin.woff2 +0 -0
  74. package/dist/ingest/racing-game/a/Inter-ThinItalic.woff +0 -0
  75. package/dist/ingest/racing-game/a/Inter-ThinItalic.woff2 +0 -0
  76. package/dist/ingest/racing-game/a/Inter-italic.var.woff2 +0 -0
  77. package/dist/ingest/racing-game/a/Inter-roman.var.woff2 +0 -0
  78. package/dist/ingest/racing-game/a/Inter.var.woff2 +0 -0
  79. package/dist/ingest/racing-game/a/index.css +1 -0
  80. package/dist/ingest/racing-game/images/bronze.png +0 -0
  81. package/dist/ingest/racing-game/images/gold.png +0 -0
  82. package/dist/ingest/racing-game/images/silver.png +0 -0
  83. package/dist/ingest/racing-game/index.html +15 -0
  84. package/dist/ingest/racing-game/models/chassis-draco.glb +0 -0
  85. package/dist/ingest/racing-game/models/track-draco.glb +0 -0
  86. package/dist/ingest/racing-game/models/wheel-draco.glb +0 -0
  87. package/dist/ingest/racing-game/rg-entry.js +1 -0
  88. package/dist/ingest/racing-game/sounds/accelerate.mp3 +0 -0
  89. package/dist/ingest/racing-game/sounds/boost.mp3 +0 -0
  90. package/dist/ingest/racing-game/sounds/crash.mp3 +0 -0
  91. package/dist/ingest/racing-game/sounds/engine.mp3 +0 -0
  92. package/dist/ingest/racing-game/sounds/honk.mp3 +0 -0
  93. package/dist/ingest/racing-game/sounds/tire-brake.mp3 +0 -0
  94. package/dist/ingest/racing-game/sounds/train.mp3 +0 -0
  95. package/dist/ingest/racing-game/sounds/water.mp3 +0 -0
  96. package/dist/ingest/racing-game/textures/cursor.svg +3 -0
  97. package/dist/ingest/racing-game/textures/dikhololo_night_1k.hdr +0 -0
  98. package/dist/ingest/racing-game/textures/heightmap_1024.png +0 -0
  99. package/dist/ingest/racing-game/textures/mask.svg +11 -0
  100. package/dist/ingest/racing-game/textures/no_sound.png +0 -0
  101. package/dist/ingest/racing-game/three-r139.module.js +50617 -0
  102. package/dist/ingest/racing-game/vendor.js +179 -0
  103. package/dist/ingest/racing-game/vgai.game.json +20 -0
  104. package/dist/ingest/simcity/UPSTREAM.md +65 -0
  105. package/dist/ingest/simcity/favicon.ico +0 -0
  106. package/dist/ingest/simcity/fonts/Bebas-Regular.otf +0 -0
  107. package/dist/ingest/simcity/fonts/Digitalt.otf +0 -0
  108. package/dist/ingest/simcity/fonts/Stanberry.ttf +0 -0
  109. package/dist/ingest/simcity/icons/bulldozer-color.png +0 -0
  110. package/dist/ingest/simcity/icons/bulldozer.png +0 -0
  111. package/dist/ingest/simcity/icons/calendar.png +0 -0
  112. package/dist/ingest/simcity/icons/factory-color.png +0 -0
  113. package/dist/ingest/simcity/icons/factory.png +0 -0
  114. package/dist/ingest/simcity/icons/house-color.png +0 -0
  115. package/dist/ingest/simcity/icons/house.png +0 -0
  116. package/dist/ingest/simcity/icons/job.png +0 -0
  117. package/dist/ingest/simcity/icons/office.png +0 -0
  118. package/dist/ingest/simcity/icons/pause-color.png +0 -0
  119. package/dist/ingest/simcity/icons/pause.png +0 -0
  120. package/dist/ingest/simcity/icons/person.png +0 -0
  121. package/dist/ingest/simcity/icons/play-color.png +0 -0
  122. package/dist/ingest/simcity/icons/play.png +0 -0
  123. package/dist/ingest/simcity/icons/power-color.png +0 -0
  124. package/dist/ingest/simcity/icons/power-line-color.png +0 -0
  125. package/dist/ingest/simcity/icons/power.png +0 -0
  126. package/dist/ingest/simcity/icons/road-color.png +0 -0
  127. package/dist/ingest/simcity/icons/road.png +0 -0
  128. package/dist/ingest/simcity/icons/select-color.png +0 -0
  129. package/dist/ingest/simcity/icons/select.png +0 -0
  130. package/dist/ingest/simcity/icons/store-color.png +0 -0
  131. package/dist/ingest/simcity/index.html +94 -0
  132. package/dist/ingest/simcity/index.js +58 -0
  133. package/dist/ingest/simcity/main.css +233 -0
  134. package/dist/ingest/simcity/models/armored-truck.glb +0 -0
  135. package/dist/ingest/simcity/models/building-block-4floor-corner.glb +0 -0
  136. package/dist/ingest/simcity/models/building-block-4floor-short.glb +0 -0
  137. package/dist/ingest/simcity/models/building-block-5floor.glb +0 -0
  138. package/dist/ingest/simcity/models/building-burger-joint.glb +0 -0
  139. package/dist/ingest/simcity/models/building-cafe.glb +0 -0
  140. package/dist/ingest/simcity/models/building-casino.glb +0 -0
  141. package/dist/ingest/simcity/models/building-cinema.glb +0 -0
  142. package/dist/ingest/simcity/models/building-house-block-big.glb +0 -0
  143. package/dist/ingest/simcity/models/building-house-family-large.glb +0 -0
  144. package/dist/ingest/simcity/models/building-house-family-small.glb +0 -0
  145. package/dist/ingest/simcity/models/building-office-balcony.glb +0 -0
  146. package/dist/ingest/simcity/models/building-office-big.glb +0 -0
  147. package/dist/ingest/simcity/models/building-office-pyramid.glb +0 -0
  148. package/dist/ingest/simcity/models/building-office-tall.glb +0 -0
  149. package/dist/ingest/simcity/models/building-office.glb +0 -0
  150. package/dist/ingest/simcity/models/building-restaurant.glb +0 -0
  151. package/dist/ingest/simcity/models/building-skyscraper.glb +0 -0
  152. package/dist/ingest/simcity/models/car-ambulance-pickup.glb +0 -0
  153. package/dist/ingest/simcity/models/car-baywatch.glb +0 -0
  154. package/dist/ingest/simcity/models/car-hippie-van.glb +0 -0
  155. package/dist/ingest/simcity/models/car-passenger-race.glb +0 -0
  156. package/dist/ingest/simcity/models/car-passenger.glb +0 -0
  157. package/dist/ingest/simcity/models/car-police.glb +0 -0
  158. package/dist/ingest/simcity/models/car-taxi.glb +0 -0
  159. package/dist/ingest/simcity/models/car-tow-truck.glb +0 -0
  160. package/dist/ingest/simcity/models/car-truck-dump.glb +0 -0
  161. package/dist/ingest/simcity/models/car-veteran.glb +0 -0
  162. package/dist/ingest/simcity/models/construction-small.glb +0 -0
  163. package/dist/ingest/simcity/models/data-center.glb +0 -0
  164. package/dist/ingest/simcity/models/industry-factory-old.glb +0 -0
  165. package/dist/ingest/simcity/models/industry-factory.glb +0 -0
  166. package/dist/ingest/simcity/models/industry-refinery.glb +0 -0
  167. package/dist/ingest/simcity/models/industry-warehouse.glb +0 -0
  168. package/dist/ingest/simcity/models/power_line_pole_modified.glb +0 -0
  169. package/dist/ingest/simcity/models/tile-plain_grass.glb +0 -0
  170. package/dist/ingest/simcity/models/tile-road-curve.glb +0 -0
  171. package/dist/ingest/simcity/models/tile-road-end.glb +0 -0
  172. package/dist/ingest/simcity/models/tile-road-intersection-t.glb +0 -0
  173. package/dist/ingest/simcity/models/tile-road-intersection.glb +0 -0
  174. package/dist/ingest/simcity/models/tile-road-straight.glb +0 -0
  175. package/dist/ingest/simcity/models/truck.glb +0 -0
  176. package/dist/ingest/simcity/statusIcons/no-power.png +0 -0
  177. package/dist/ingest/simcity/statusIcons/no-road-access.png +0 -0
  178. package/dist/ingest/simcity/textures/base.png +0 -0
  179. package/dist/ingest/simcity/textures/grid.png +0 -0
  180. package/dist/ingest/simcity/textures/specular.png +0 -0
  181. package/dist/ingest/simcity/three-r155.module.js +51466 -0
  182. package/dist/ingest/simcity/vgai.game.json +20 -0
  183. package/dist/ingest/tanks/UPSTREAM.md +69 -0
  184. package/dist/ingest/tanks/index.html +214 -0
  185. package/dist/ingest/tanks/models/pixelTank.fbx +0 -0
  186. package/dist/ingest/tanks/tanks-entry.js +92 -0
  187. package/dist/ingest/tanks/three-r170.module.js +54571 -0
  188. package/dist/ingest/tanks/vgai.game.json +20 -0
  189. package/dist/inputmaps/default.inputmap.json +41 -0
  190. package/dist/jsm/libs/draco/gltf/draco_decoder.js +33 -0
  191. package/dist/jsm/libs/draco/gltf/draco_decoder.wasm +0 -0
  192. package/dist/jsm/libs/draco/gltf/draco_wasm_wrapper.js +116 -0
  193. package/dist/scenes/untitled.vscn.json +29 -0
  194. package/dist/textures/waternormals.jpg +0 -0
  195. package/dist-server/packaged.mjs +18155 -0
  196. package/package.json +36 -0
  197. package/server/asset-library-routes.ts +833 -0
  198. package/server/canonical-path.ts +50 -0
  199. package/server/cloud-asset-catalog.ts +101 -0
  200. package/server/dev.ts +350 -0
  201. package/server/editor-server.ts +1537 -0
  202. package/server/editor-sse.ts +73 -0
  203. package/server/game-globals-shadow.ts +58 -0
  204. package/server/local-asset-catalog.ts +163 -0
  205. package/server/packaged.ts +571 -0
  206. package/server/prod.ts +75 -0
  207. package/server/project-optimize-deps-entries.ts +149 -0
  208. package/server/project-view.ts +65 -0
  209. package/server/server-utils.ts +430 -0
  210. package/server/session-registry.ts +93 -0
  211. package/src/EditorContext.tsx +87 -0
  212. package/src/achieved-tier.ts +126 -0
  213. package/src/action-registry.ts +268 -0
  214. package/src/adapter-resolver.ts +1858 -0
  215. package/src/asset-events.ts +100 -0
  216. package/src/authoring/active-adapter.ts +98 -0
  217. package/src/authoring/active-systems.ts +50 -0
  218. package/src/authoring/boundary-authoring-adapter.ts +178 -0
  219. package/src/authoring/composite-authoring-adapter.ts +891 -0
  220. package/src/authoring/design-time-layers.ts +776 -0
  221. package/src/authoring/edit-mode-authoring.ts +643 -0
  222. package/src/authoring/embed-only-adapter.ts +57 -0
  223. package/src/authoring/ephemeral-persistence.ts +26 -0
  224. package/src/authoring/eyedropper-session.ts +69 -0
  225. package/src/authoring/first-party-hierarchy-menu.ts +74 -0
  226. package/src/authoring/first-party-inspector/AddSectionMenu.tsx +118 -0
  227. package/src/authoring/first-party-inspector/EnvironmentSection.tsx +550 -0
  228. package/src/authoring/first-party-inspector/HeaderSection.tsx +329 -0
  229. package/src/authoring/first-party-inspector/is-first-party-selection.ts +44 -0
  230. package/src/authoring/first-party-inspector/register.tsx +108 -0
  231. package/src/authoring/ingest-authoring-adapter.ts +475 -0
  232. package/src/authoring/ingest-capture.ts +94 -0
  233. package/src/authoring/ingest-game-adapter.ts +291 -0
  234. package/src/authoring/ingest-iframe-adapter.ts +38 -0
  235. package/src/authoring/ingest-iframe-reachable-adapter.ts +401 -0
  236. package/src/authoring/ingest-overlay.ts +193 -0
  237. package/src/authoring/layered-pick.ts +87 -0
  238. package/src/authoring/mount-failure-report.ts +112 -0
  239. package/src/authoring/overlay-report.ts +52 -0
  240. package/src/authoring/project-scenes.ts +111 -0
  241. package/src/authoring/react-dom-authoring-adapter.ts +795 -0
  242. package/src/authoring/react-inspector-section.tsx +1041 -0
  243. package/src/authoring/react-world-authoring-adapter.ts +2400 -0
  244. package/src/authoring/root-activation.ts +61 -0
  245. package/src/authoring/scene-ui-layer.ts +485 -0
  246. package/src/authoring/story-overlay.ts +48 -0
  247. package/src/authoring/ui-authoring-adapter.ts +325 -0
  248. package/src/authoring/ui-hierarchy-menu.ts +40 -0
  249. package/src/authoring/ui-inspector-sections.tsx +156 -0
  250. package/src/authoring/undo-timeline.ts +145 -0
  251. package/src/authoring/vgai-scene-authoring-adapter.ts +746 -0
  252. package/src/authoring/viewport-pick-context.ts +31 -0
  253. package/src/authoring/viewport-raycast.ts +80 -0
  254. package/src/authoring/world-overlay.ts +76 -0
  255. package/src/authoring/world-pan-state.ts +115 -0
  256. package/src/authoring/world-session-state.ts +102 -0
  257. package/src/authoring/world2d-authoring-adapter.ts +313 -0
  258. package/src/browser-external-watch.ts +98 -0
  259. package/src/browser-play-entry.ts +112 -0
  260. package/src/browser-project-scripts.ts +112 -0
  261. package/src/browser-transpile.ts +246 -0
  262. package/src/browser-watch-reload.ts +100 -0
  263. package/src/command-listener.ts +326 -0
  264. package/src/components/AlignToolbar.tsx +189 -0
  265. package/src/components/AppRoot.tsx +73 -0
  266. package/src/components/AssetBrowser.tsx +502 -0
  267. package/src/components/BottomPanel.tsx +326 -0
  268. package/src/components/BuildPanel.tsx +455 -0
  269. package/src/components/CameraInfo.tsx +56 -0
  270. package/src/components/CommandPalette.tsx +265 -0
  271. package/src/components/ConsolePanel.tsx +272 -0
  272. package/src/components/DataPanel.tsx +1268 -0
  273. package/src/components/DefaultEditorLayout.tsx +509 -0
  274. package/src/components/FolderTree.tsx +179 -0
  275. package/src/components/GameHierarchy.tsx +1349 -0
  276. package/src/components/Inspector.tsx +671 -0
  277. package/src/components/InspectorToolSection.tsx +74 -0
  278. package/src/components/MountFailureBanner.tsx +110 -0
  279. package/src/components/NetworkSection.tsx +109 -0
  280. package/src/components/OnlineAssetBrowser.tsx +448 -0
  281. package/src/components/OverlayReportBanner.tsx +122 -0
  282. package/src/components/PlayBar.tsx +356 -0
  283. package/src/components/PrefabSaveDialog.tsx +201 -0
  284. package/src/components/ProjectHeader.tsx +174 -0
  285. package/src/components/ProjectScreen.tsx +1004 -0
  286. package/src/components/RightPanel.tsx +166 -0
  287. package/src/components/RootActivateDialog.tsx +105 -0
  288. package/src/components/SaveDialog.tsx +197 -0
  289. package/src/components/SaveStatus.tsx +69 -0
  290. package/src/components/StatsOverlay.tsx +35 -0
  291. package/src/components/ToolHost.tsx +80 -0
  292. package/src/components/Toolbar.tsx +307 -0
  293. package/src/components/ViewportOverlay.tsx +234 -0
  294. package/src/components/ViewportPanel.tsx +369 -0
  295. package/src/components/WorldContextMenu.tsx +257 -0
  296. package/src/components/WorldSelectionOverlay.tsx +2003 -0
  297. package/src/components/WorldTextEditor.tsx +86 -0
  298. package/src/components/asset-viewers/AssetViewer.tsx +38 -0
  299. package/src/components/asset-viewers/AudioViewer.tsx +149 -0
  300. package/src/components/asset-viewers/GenericJsonViewer.tsx +77 -0
  301. package/src/components/asset-viewers/ImageViewer.tsx +65 -0
  302. package/src/components/asset-viewers/InputMapViewer.tsx +206 -0
  303. package/src/components/asset-viewers/ModelViewer.tsx +180 -0
  304. package/src/components/asset-viewers/OnlineAssetDetail.tsx +236 -0
  305. package/src/components/data-csv.ts +301 -0
  306. package/src/components/entity-name-commit.ts +18 -0
  307. package/src/components/inspector-property-grouping.ts +65 -0
  308. package/src/components/inspector-widgets/AlignmentGrid.tsx +188 -0
  309. package/src/components/inspector-widgets/BorderEditor.tsx +305 -0
  310. package/src/components/inspector-widgets/ColorPicker.tsx +548 -0
  311. package/src/components/inspector-widgets/FilterEditor.tsx +108 -0
  312. package/src/components/inspector-widgets/FontPicker.tsx +196 -0
  313. package/src/components/inspector-widgets/GradientEditor.tsx +277 -0
  314. package/src/components/inspector-widgets/ScrubbableInput.tsx +152 -0
  315. package/src/components/inspector-widgets/ShadowEditor.tsx +332 -0
  316. package/src/components/inspector-widgets/color-utils.ts +202 -0
  317. package/src/components/inspector-widgets/gallery-main.tsx +167 -0
  318. package/src/components/inspector-widgets/index.ts +22 -0
  319. package/src/components/inspector-widgets/shared.tsx +135 -0
  320. package/src/components/inspectors/AnimationSection.tsx +143 -0
  321. package/src/components/inspectors/AudioSection.tsx +215 -0
  322. package/src/components/inspectors/BoneAttachmentSection.tsx +82 -0
  323. package/src/components/inspectors/CameraSection.tsx +105 -0
  324. package/src/components/inspectors/ComponentsSection.tsx +358 -0
  325. package/src/components/inspectors/JointsSection.tsx +255 -0
  326. package/src/components/inspectors/LightSection.tsx +116 -0
  327. package/src/components/inspectors/MaterialSection.tsx +1189 -0
  328. package/src/components/inspectors/MeshSection.tsx +99 -0
  329. package/src/components/inspectors/NavigationSection.tsx +58 -0
  330. package/src/components/inspectors/ParticleSection.tsx +681 -0
  331. package/src/components/inspectors/PhysicsSection.tsx +326 -0
  332. package/src/components/inspectors/PostProcessingSection.tsx +1290 -0
  333. package/src/components/inspectors/RenderingSection.tsx +109 -0
  334. package/src/components/inspectors/ShadowSection.tsx +84 -0
  335. package/src/components/inspectors/SplineSection.tsx +191 -0
  336. package/src/components/inspectors/TransformSection.tsx +191 -0
  337. package/src/components/inspectors/json-schema-fields.ts +281 -0
  338. package/src/components/inspectors/multi-edit-utils.ts +49 -0
  339. package/src/components/inspectors/zod-fields.ts +184 -0
  340. package/src/components/primitives/ColorInput.tsx +109 -0
  341. package/src/components/primitives/JsonInput.tsx +98 -0
  342. package/src/components/primitives/NumberInput.tsx +164 -0
  343. package/src/components/primitives/Panel.tsx +68 -0
  344. package/src/components/primitives/SectionHeader.tsx +45 -0
  345. package/src/components/primitives/Tooltip.tsx +82 -0
  346. package/src/components/primitives/Vec3Input.tsx +62 -0
  347. package/src/components/world-overlay-gestures.ts +1438 -0
  348. package/src/doctor/data-checks.ts +285 -0
  349. package/src/doctor/report.ts +386 -0
  350. package/src/editor-api.ts +700 -0
  351. package/src/editor-console.ts +194 -0
  352. package/src/editor-hotkeys.ts +441 -0
  353. package/src/editor-layers.ts +2 -0
  354. package/src/editor-mode.ts +28 -0
  355. package/src/editor-store.ts +2225 -0
  356. package/src/editor-viewport.ts +1924 -0
  357. package/src/entity-factory.ts +221 -0
  358. package/src/game-globals-prelude.ts +27 -0
  359. package/src/gated-globals.ts +131 -0
  360. package/src/generated/setup-by-example.generated.ts +53 -0
  361. package/src/gizmo-registry.ts +608 -0
  362. package/src/handle-system.ts +221 -0
  363. package/src/hierarchy-menu-registry.ts +70 -0
  364. package/src/hierarchy-rows.ts +73 -0
  365. package/src/hosted-example.ts +160 -0
  366. package/src/hotkeys.ts +142 -0
  367. package/src/ingest/discovery-react.ts +132 -0
  368. package/src/ingest/discovery.ts +205 -0
  369. package/src/ingest/discovery2d.ts +204 -0
  370. package/src/ingest/esm-entry-resolution.ts +102 -0
  371. package/src/ingest/games/bloom-composer/game.js +99 -0
  372. package/src/ingest/games/bloom-composer/vgai.game.json +21 -0
  373. package/src/ingest/games/embed-bundle/embed.html +10 -0
  374. package/src/ingest/games/embed-bundle/vgai.game.json +21 -0
  375. package/src/ingest/games/games-fps/assets/collision-world.glb +0 -0
  376. package/src/ingest/games/games-fps/game.js +479 -0
  377. package/src/ingest/games/games-fps/vgai.game.json +24 -0
  378. package/src/ingest/games/iframe-reachable/game-source.js +58 -0
  379. package/src/ingest/games/iframe-reachable/vgai.game.json +21 -0
  380. package/src/ingest/games/interactive-cubes/game.js +139 -0
  381. package/src/ingest/games/interactive-cubes/vgai.game.json +21 -0
  382. package/src/ingest/games/mismatched-three/fake-three.js +38 -0
  383. package/src/ingest/games/mismatched-three/game.js +10 -0
  384. package/src/ingest/games/mismatched-three/vgai.game.json +22 -0
  385. package/src/ingest/games/own-three-bundled/game.js +43 -0
  386. package/src/ingest/games/own-three-bundled/vgai.game.json +21 -0
  387. package/src/ingest/games/pointerlock-maze/game.js +288 -0
  388. package/src/ingest/games/pointerlock-maze/vgai.game.json +22 -0
  389. package/src/ingest/games-2d/_fixtures/first-party-scene.scn2d.json +124 -0
  390. package/src/ingest/games-2d/bubbo-bubbo/vgai.game.json +25 -0
  391. package/src/ingest/games-2d/bunnymark/game.ts +88 -0
  392. package/src/ingest/games-2d/bunnymark/vgai.game.json +21 -0
  393. package/src/ingest/games-2d/flappy/game.ts +148 -0
  394. package/src/ingest/games-2d/flappy/vgai.game.json +21 -0
  395. package/src/ingest/games-2d/flappy-pixi/vgai.game.json +25 -0
  396. package/src/ingest/games-2d/puzzling-potions/vgai.game.json +25 -0
  397. package/src/ingest/games-2d/tilemap-camera/game.ts +115 -0
  398. package/src/ingest/games-2d/tilemap-camera/vgai.game.json +21 -0
  399. package/src/ingest/games-react/react-rpg/entry.tsx +185 -0
  400. package/src/ingest/games-react/react-rpg/shims.d.ts +26 -0
  401. package/src/ingest/games-react/react-rpg/src/ui/game-state.tsx +129 -0
  402. package/src/ingest/games-react/react-rpg/vgai.game.json +21 -0
  403. package/src/ingest/host-namespace-registry.ts +196 -0
  404. package/src/ingest/types.ts +127 -0
  405. package/src/ingest-hierarchy-rows.ts +207 -0
  406. package/src/ingest-mode.ts +2092 -0
  407. package/src/ingest-siblings.ts +466 -0
  408. package/src/initial-scene.ts +194 -0
  409. package/src/inspector-section-registry.ts +117 -0
  410. package/src/interactive-edit-scope.ts +34 -0
  411. package/src/main.tsx +23 -0
  412. package/src/manifest-project.ts +43 -0
  413. package/src/model-thumbnail.ts +156 -0
  414. package/src/module-mode.ts +513 -0
  415. package/src/navmesh-handler.ts +94 -0
  416. package/src/packaged-runtime.ts +49 -0
  417. package/src/play-mode.ts +1148 -0
  418. package/src/playground/main.ts +206 -0
  419. package/src/project-manager.ts +173 -0
  420. package/src/scene-diff.ts +112 -0
  421. package/src/scene-serializer.ts +144 -0
  422. package/src/scene-sync.ts +587 -0
  423. package/src/scene-ui-mode/active-scene-ui.ts +58 -0
  424. package/src/scene-ui-mode/scene-ui-palette-panel.tsx +152 -0
  425. package/src/scene-ui-mode/scene-ui-timeline-tab.tsx +29 -0
  426. package/src/scene-ui-mode/ui-prefab-dialog-host.tsx +74 -0
  427. package/src/scene-ui-mode/ui-prefab-hierarchy-menu.ts +49 -0
  428. package/src/scene-ui-mode/ui-prefab-io.ts +76 -0
  429. package/src/script-loader.ts +143 -0
  430. package/src/storage/fsa-storage.ts +55 -0
  431. package/src/storage/handle-storage.ts +150 -0
  432. package/src/storage/handle-store.ts +116 -0
  433. package/src/storage/http-storage.ts +137 -0
  434. package/src/storage/index.ts +147 -0
  435. package/src/storage/mem-storage.ts +135 -0
  436. package/src/storage/opfs-storage.ts +25 -0
  437. package/src/storage/paths.ts +26 -0
  438. package/src/storage/seed.ts +106 -0
  439. package/src/storage/types.ts +110 -0
  440. package/src/tool-loader.ts +291 -0
  441. package/src/ui-editor/inspector.tsx +563 -0
  442. package/src/ui-editor/node-ops.ts +266 -0
  443. package/src/ui-editor/overlay.tsx +380 -0
  444. package/src/ui-editor/palette.tsx +114 -0
  445. package/src/ui-editor/react-store.ts +78 -0
  446. package/src/ui-editor/timeline-panel.tsx +443 -0
  447. package/src/ui-editor/ui-edit-store.ts +552 -0
  448. package/src/ui-editor/ui-prefab.ts +95 -0
  449. package/src/ui-source/inspect.ts +544 -0
  450. package/src/ui-source/oid-transform.ts +133 -0
  451. package/src/ui-source/source-write-backend.ts +203 -0
  452. package/src/ui-source/tailwind-map.ts +174 -0
  453. package/src/ui-source/writer.ts +1175 -0
  454. package/src/widgets/index.ts +73 -0
  455. package/template/.claude/skills/editor/SKILL.md +252 -0
  456. package/template/CLAUDE.md +512 -0
  457. package/template/LICENSE +30 -0
  458. package/template/emit-schemas.ts +42 -0
  459. package/template/index.html +17 -0
  460. package/template/package.json +53 -0
  461. package/template/project.json +9 -0
  462. package/template/public/audio/ambient-arena.wav +0 -0
  463. package/template/public/audio/ambient-music.wav +0 -0
  464. package/template/public/audio/ambient-nature.wav +0 -0
  465. package/template/public/audio/ambient-wind.wav +0 -0
  466. package/template/public/inputmaps/default.inputmap.json +41 -0
  467. package/template/public/jsm/libs/draco/gltf/draco_decoder.js +33 -0
  468. package/template/public/jsm/libs/draco/gltf/draco_decoder.wasm +0 -0
  469. package/template/public/jsm/libs/draco/gltf/draco_encoder.js +33 -0
  470. package/template/public/jsm/libs/draco/gltf/draco_wasm_wrapper.js +116 -0
  471. package/template/public/scenes/default.vscn.json +43 -0
  472. package/template/public/scenes/untitled.vscn.json +29 -0
  473. package/template/public/textures/waternormals.jpg +0 -0
  474. package/template/server/colyseus-setup.ts +85 -0
  475. package/template/server/main.ts +34 -0
  476. package/template/server/rooms/arena-room.ts +420 -0
  477. package/template/server/rooms/game-room.ts +162 -0
  478. package/template/server/rooms.ts +15 -0
  479. package/template/server/tsconfig.json +19 -0
  480. package/template/src/data/README.md +40 -0
  481. package/template/src/data/assets.ts +31 -0
  482. package/template/src/data/tuning.data.json +5 -0
  483. package/template/src/data/tuning.schema.json +21 -0
  484. package/template/src/data/tuning.schema.ts +34 -0
  485. package/template/src/data/tuning.ts +24 -0
  486. package/template/src/main.ts +157 -0
  487. package/template/src/scripts/components/data-spinner.ts +45 -0
  488. package/template/src/scripts/components/scene-camera.ts +40 -0
  489. package/template/src/scripts/main.ts +94 -0
  490. package/template/src/scripts/registry.ts +46 -0
  491. package/template/src/tools/README.md +14 -0
  492. package/template/src/tools/example.tool.tsx +91 -0
  493. package/template/src/tools/spin.tool.tsx +101 -0
  494. package/template/src/ui/Button.tsx +27 -0
  495. package/template/src/ui/DefaultHud.tsx +40 -0
  496. package/template/src/ui/KeyHint.tsx +25 -0
  497. package/template/src/ui/Panel.tsx +52 -0
  498. package/template/src/ui/RebindPanel.tsx +129 -0
  499. package/template/src/ui/Slider.tsx +28 -0
  500. package/template/src/ui/StatusDot.tsx +22 -0
  501. package/template/src/ui/Toggle.tsx +19 -0
  502. package/template/src/ui/game-state.tsx +37 -0
  503. package/template/src/ui/index.ts +7 -0
  504. package/template/src/ui/mount-default-hud.tsx +19 -0
  505. package/template/src/ui/mount-ui.ts +15 -0
  506. package/template/tsconfig.json +36 -0
  507. package/template/validate-asset-content.ts +78 -0
  508. package/template/validate-manifest.ts +55 -0
  509. package/template/validate-scenes.ts +211 -0
  510. package/template/vgai.game.json +21 -0
  511. package/template/vite.config.ts +128 -0
  512. package/vite-plugin-example-assets.ts +60 -0
  513. package/vite-plugin-game-globals.ts +46 -0
  514. package/vite-plugin-game-static.ts +71 -0
  515. package/vite-plugin-project-game-static.ts +84 -0
  516. package/vite-plugin-react-game-static.ts +98 -0
  517. package/vite-plugin-react-world-provider.ts +81 -0
  518. package/vite-plugin-react-world-runtime.ts +76 -0
  519. package/vite-plugin-ui-oid.ts +561 -0
@@ -0,0 +1,285 @@
1
+ /**
2
+ * `vgai doctor` — data-asset checks (docs/DATA-TOOLS-DESIGN.md §6.6, the
3
+ * enforcement half of W5). PURE data-in/data-out, exactly like its sibling
4
+ * `report.ts`: the fs/schema-executing scraping lives in
5
+ * `packages/editor/scripts/doctor-data-scan.ts` (spawned under `tsx`, which
6
+ * is what lets it dynamic-import the project's `.schema.ts` modules), and
7
+ * hands this module plain {@link ScannedDataProject} data — so the check
8
+ * RULES are unit-testable without a filesystem or a project.
9
+ *
10
+ * The checks, severities fixed by the spec (§6.6):
11
+ *
12
+ * - **data file fails schema** → ERROR (flips the doctor exit code). Also
13
+ * covers a `.data.json` that isn't valid JSON at all.
14
+ * - **dangling `file#key` ref** → ERROR. Shared definition with the
15
+ * build-path plugin via the engine's `findDanglingDataRefs` (a ref only
16
+ * "exists" once its file prefix names a real data asset — see
17
+ * `data-check-core.ts`'s calibration note).
18
+ * - **declared `dataRef(target)` field's target file missing**
19
+ * (`'data-ref-missing-file'`) → ERROR — the §9.4 fix. Unlike the
20
+ * conventional-string check above, this fires purely from the SCHEMA's
21
+ * declared intent (`"x-vgai-ref": target` on the emitted field), so
22
+ * renaming/deleting the target file no longer silently un-checks the
23
+ * ref — see `collectDeclaredRefFields`/`findMissingRefTargets` in
24
+ * `data-check-core.ts`.
25
+ * - **`.data.json` without a schema** → WARN. Two shapes: no governing
26
+ * `.schema.ts` at all, or a `.schema.ts` whose emitted `.schema.json`
27
+ * was never generated (D4 default is generated-and-committed).
28
+ * - **zero data assets in a non-trivial project** → INFO nudge (the
29
+ * doctrine's "deleting the pattern is a choice" — §6.2).
30
+ *
31
+ * Anti-shim doctrine (same as report.ts): a schema that could not be
32
+ * EXECUTED (import failed, ambiguous exports) is a WARN naming the reason —
33
+ * never silently counted as validated, never fabricated into an error the
34
+ * data file didn't earn.
35
+ *
36
+ * NOTE the engine import below is RELATIVE, not `@engine/*`: this module is
37
+ * imported by `scripts/run-doctor.ts`, which runs under plain `tsx` where no
38
+ * tsconfig-path alias exists (same constraint documented at run-doctor's own
39
+ * imports).
40
+ */
41
+
42
+ import {
43
+ collectDeclaredRefFields,
44
+ findDanglingDataRefs,
45
+ findMissingRefTargets,
46
+ } from '../../../engine/src/data/data-check-core';
47
+
48
+ export type DataFindingSeverity = 'error' | 'warn' | 'info';
49
+
50
+ export type DataCheckId =
51
+ | 'data-schema' // §6.6 check 1: data file fails schema (error)
52
+ | 'data-dangling-ref' // §6.6 check 2: dangling file#key ref (error)
53
+ | 'data-missing-schema' // §6.6 check 3: .data.json without schema (warn)
54
+ | 'data-duplicate-stem' // duplicate asset stem across files (error)
55
+ | 'data-ref-missing-file' // §9.4 fix: a declared dataRef(target)'s target file doesn't exist (error)
56
+ | 'data-none'; // §6.6 check 4: zero data assets, non-trivial project (info)
57
+
58
+ export interface DataDoctorFinding {
59
+ readonly severity: DataFindingSeverity;
60
+ readonly check: DataCheckId;
61
+ /** Project-relative path of the offending file, or `null` for project-level findings. */
62
+ readonly file: string | null;
63
+ readonly message: string;
64
+ }
65
+
66
+ /** Outcome of executing an asset's `.schema.ts` against its `.data.json` — scraped by doctor-data-scan.ts. */
67
+ export type ScannedSchemaValidation =
68
+ | { readonly kind: 'ok' }
69
+ | { readonly kind: 'invalid'; readonly message: string }
70
+ /** The schema module could not be executed/identified — reason says why (import error, ambiguous exports). */
71
+ | { readonly kind: 'unavailable'; readonly reason: string };
72
+
73
+ export interface ScannedDataAsset {
74
+ /** Asset stem — filename minus `.data.json` (the name refs address it by, §2.2). */
75
+ readonly name: string;
76
+ /** Project-relative path of the `.data.json`. */
77
+ readonly dataPath: string;
78
+ /** Parsed JSON payload, or `undefined` when `jsonError` is set. */
79
+ readonly json?: unknown;
80
+ /** JSON.parse failure message, or `null` if the file parsed. */
81
+ readonly jsonError: string | null;
82
+ /** Project-relative path of the sibling `.schema.ts`, or `null` if none exists. */
83
+ readonly schemaTsPath: string | null;
84
+ /** Project-relative path of the sibling EMITTED `.schema.json`, or `null` if none exists. */
85
+ readonly schemaJsonPath: string | null;
86
+ /** Validation outcome — `null` iff there is no `.schema.ts` to validate against. */
87
+ readonly validation: ScannedSchemaValidation | null;
88
+ /**
89
+ * The emitted JSON Schema (`toDataJsonSchema` output, or the committed
90
+ * `.schema.json` twin when the module couldn't be executed) — present
91
+ * whenever EITHER is available, independent of `validation`'s outcome.
92
+ * Used only to find declared `dataRef(target)` fields (§9.4 fix); `null`/
93
+ * absent degrades to "no declared refs found here", never an error.
94
+ */
95
+ readonly emittedSchema?: Record<string, unknown>;
96
+ }
97
+
98
+ export interface ScannedDataProject {
99
+ readonly assets: readonly ScannedDataAsset[];
100
+ /** Count of `.ts`/`.tsx` files under the project's `src/` — the non-triviality signal. */
101
+ readonly sourceFileCount: number;
102
+ }
103
+
104
+ /**
105
+ * "Non-trivial project" calibration for the §6.6 info nudge: the template
106
+ * scaffolds ~17 source files INCLUDING the data trio, so a project that
107
+ * deleted the pattern still gets nudged, while a three-file experiment does
108
+ * not. Deliberately a soft signal — it gates an INFO, nothing stronger.
109
+ */
110
+ export const NON_TRIVIAL_SOURCE_FILE_COUNT = 10;
111
+
112
+ /**
113
+ * Every `.data.json`'s asset "name" is its STEM (filename minus the
114
+ * extension, §2.2) — the same identity `file#key` refs address a file by.
115
+ * `src/data/` is walked recursively (`scanProjectDataAssets`), so two files
116
+ * in different subdirectories can share a stem (e.g. `src/data/enemies` and
117
+ * `src/data/legacy/enemies`) — an unresolvable identity collision that the
118
+ * downstream stem-keyed `Map` (ref resolution, below) would otherwise
119
+ * silently paper over with last-wins, while `.find()`-based error attribution
120
+ * elsewhere would report the FIRST one — two different silent tie-breaks
121
+ * disagreeing with each other. This must be a hard error naming every
122
+ * colliding path, not a silent collision.
123
+ */
124
+ function findDuplicateStems(assets: readonly ScannedDataAsset[]): Map<string, ScannedDataAsset[]> {
125
+ const byStem = new Map<string, ScannedDataAsset[]>();
126
+ for (const asset of assets) {
127
+ const group = byStem.get(asset.name);
128
+ if (group) group.push(asset);
129
+ else byStem.set(asset.name, [asset]);
130
+ }
131
+ for (const [stem, group] of [...byStem]) {
132
+ if (group.length < 2) byStem.delete(stem);
133
+ }
134
+ return byStem;
135
+ }
136
+
137
+ /** Assemble the §6.6 findings from a scanned project. Deterministic: per-asset checks in scan order, then refs, then the project-level nudge. */
138
+ export function assembleDataFindings(scanned: ScannedDataProject): DataDoctorFinding[] {
139
+ const findings: DataDoctorFinding[] = [];
140
+
141
+ for (const asset of scanned.assets) {
142
+ findings.push(...perAssetFindings(asset));
143
+ }
144
+
145
+ const duplicateStems = findDuplicateStems(scanned.assets);
146
+ for (const [stem, group] of duplicateStems) {
147
+ const paths = group.map((a) => a.dataPath).join(', ');
148
+ for (const asset of group) {
149
+ findings.push({
150
+ severity: 'error',
151
+ check: 'data-duplicate-stem',
152
+ file: asset.dataPath,
153
+ message:
154
+ `asset stem "${stem}" is claimed by ${group.length} files (${paths}) — "file#key" refs ` +
155
+ 'address a data asset by its stem alone (§2.2), so this is an unresolvable identity ' +
156
+ 'collision. Rename one; asset stems must be unique across the whole project, not just ' +
157
+ 'per directory.',
158
+ });
159
+ }
160
+ }
161
+
162
+ // Dangling refs — resolved across every file that parsed (an unparseable
163
+ // file is already an error above and cannot be resolved against). A
164
+ // duplicate-stem asset is excluded here too: it already has its own hard
165
+ // error above, and resolving refs against "whichever file happened to
166
+ // scan last" would be an arbitrary, possibly-wrong answer.
167
+ const parsed = new Map<string, unknown>();
168
+ for (const asset of scanned.assets) {
169
+ if (asset.jsonError === null && !duplicateStems.has(asset.name)) {
170
+ parsed.set(asset.name, asset.json);
171
+ }
172
+ }
173
+ for (const dangling of findDanglingDataRefs(parsed)) {
174
+ const inAsset = scanned.assets.find((a) => a.name === dangling.inAsset);
175
+ const target = scanned.assets.find((a) => a.name === dangling.targetAsset);
176
+ findings.push({
177
+ severity: 'error',
178
+ check: 'data-dangling-ref',
179
+ file: inAsset?.dataPath ?? null,
180
+ message:
181
+ `dangling ref at ${dangling.atPath}: "${dangling.ref}" — ` +
182
+ `"${dangling.missingSegment}" does not exist in ${target?.dataPath ?? `${dangling.targetAsset}.data.json`}. ` +
183
+ 'Fix the key or remove the ref (refs are "file#key" strings — docs/DATA-TOOLS-DESIGN.md §2.2).',
184
+ });
185
+ }
186
+
187
+ // §9.4 fix: a DECLARED `dataRef(target)` field (schema carries
188
+ // `"x-vgai-ref": target`) whose target file doesn't exist, checked by
189
+ // NAME regardless of whether any current value looks like a ref — unlike
190
+ // the dangling-ref check above, this fires even on an asset with no data
191
+ // yet, and survives a target file being renamed/deleted (the exact §9.4
192
+ // hole conventional-string detection couldn't close). A file that exists
193
+ // (even a duplicate-stem one, already flagged above) is never re-flagged
194
+ // here — the check is purely "does the name resolve to SOME file."
195
+ const allAssetNames = new Set(scanned.assets.map((a) => a.name));
196
+ for (const asset of scanned.assets) {
197
+ if (!asset.emittedSchema) continue;
198
+ const declared = collectDeclaredRefFields(asset.emittedSchema);
199
+ for (const missing of findMissingRefTargets(declared, allAssetNames)) {
200
+ findings.push({
201
+ severity: 'error',
202
+ check: 'data-ref-missing-file',
203
+ file: asset.dataPath,
204
+ message:
205
+ `field "${missing.fieldPath}" is declared as dataRef('${missing.targetStem}'), but no ` +
206
+ `"src/data/${missing.targetStem}.data.json" exists. Restore/rename the target file, or ` +
207
+ `update the dataRef('${missing.targetStem}') call in ` +
208
+ `${asset.schemaTsPath ?? 'the schema'} (docs/DATA-TOOLS-DESIGN.md §9.4).`,
209
+ });
210
+ }
211
+ }
212
+
213
+ if (scanned.assets.length === 0 && scanned.sourceFileCount >= NON_TRIVIAL_SOURCE_FILE_COUNT) {
214
+ findings.push({
215
+ severity: 'info',
216
+ check: 'data-none',
217
+ file: null,
218
+ message:
219
+ `no data assets found (src/data/*.data.json) in a project with ${scanned.sourceFileCount} ` +
220
+ 'source files. If a designer should be able to change it, it belongs in a data asset — ' +
221
+ 'a JSON file in src/data/ with a Zod schema (docs/DATA-TOOLS-DESIGN.md §0; the template ' +
222
+ 'ships the worked example). Deleting the pattern is a choice — this is a nudge, not a failure.',
223
+ });
224
+ }
225
+
226
+ return findings;
227
+ }
228
+
229
+ /** Checks 1 + 3 for one asset — factored out to keep {@link assembleDataFindings} within the lint complexity budget. */
230
+ function perAssetFindings(asset: ScannedDataAsset): DataDoctorFinding[] {
231
+ const findings: DataDoctorFinding[] = [];
232
+
233
+ if (asset.jsonError !== null) {
234
+ findings.push({
235
+ severity: 'error',
236
+ check: 'data-schema',
237
+ file: asset.dataPath,
238
+ message: `not valid JSON: ${asset.jsonError}. Fix the syntax — every consumer of this file (defineData parse-on-load, the editor Data tab, VS Code) is wedged until it parses.`,
239
+ });
240
+ } else if (asset.validation?.kind === 'invalid') {
241
+ // parseDataJson's own teaching error text, verbatim — it already names
242
+ // the file, every bad path, and the fix (§6.5 "errors teach").
243
+ findings.push({
244
+ severity: 'error',
245
+ check: 'data-schema',
246
+ file: asset.dataPath,
247
+ message: asset.validation.message,
248
+ });
249
+ } else if (asset.validation?.kind === 'unavailable') {
250
+ findings.push({
251
+ severity: 'warn',
252
+ check: 'data-schema',
253
+ file: asset.dataPath,
254
+ message:
255
+ `could not validate against ${asset.schemaTsPath ?? 'its schema'}: ${asset.validation.reason} ` +
256
+ '— not counted as a pass (never fabricated). `npm run emit-schemas` in the project is the ' +
257
+ "same check with the project's own module resolution.",
258
+ });
259
+ }
260
+
261
+ if (asset.schemaTsPath === null) {
262
+ findings.push({
263
+ severity: 'warn',
264
+ check: 'data-missing-schema',
265
+ file: asset.dataPath,
266
+ message:
267
+ 'no governing schema: expected a sibling ' +
268
+ `${asset.dataPath.replace(/\.data\.json$/, '.schema.ts')} (Zod, engineer-owned). ` +
269
+ "Every data asset is a trio — copy the template's src/data/ worked example " +
270
+ '(docs/DATA-TOOLS-DESIGN.md §2.1).',
271
+ });
272
+ } else if (asset.schemaJsonPath === null) {
273
+ findings.push({
274
+ severity: 'warn',
275
+ check: 'data-missing-schema',
276
+ file: asset.dataPath,
277
+ message:
278
+ `the emitted ${asset.dataPath.replace(/\.data\.json$/, '.schema.json')} is missing — run ` +
279
+ '`npm run emit-schemas` and commit it (D4: without it VS Code cannot validate this file ' +
280
+ 'and the editor Data tab cannot render typed widgets).',
281
+ });
282
+ }
283
+
284
+ return findings;
285
+ }
@@ -0,0 +1,386 @@
1
+ /**
2
+ * `vgai doctor <folder>` — per-world verdict assembly (D-A4,
3
+ * docs/WAVE6-DOCTOR-DESIGN.md Slice A; extended by D-H2,
4
+ * docs/WAVE9-SIBLING-PERSISTENCE-DOCTOR-DESIGN.md Slice B). PURE
5
+ * data-in/data-out: this module never touches a browser, a filesystem, or a
6
+ * child process — the browser phase (`packages/editor/scripts/run-doctor.ts`,
7
+ * NOT in the unit gate per the Slice-A NOTs) does all the scraping (spawn the
8
+ * real dev server, drive the real editor page, read the `__vgaiIngest*`/
9
+ * `__vgaiModule`/`__vgaiMountFailureReports()`/`__vgaiSiblingMounts()` window
10
+ * signals D-A3/D-H1 add/expose) and hands its findings to
11
+ * {@link assembleDoctorReport} as plain data, so the verdict RULES
12
+ * themselves — the actual "what counts as proof" logic F19 exists to get
13
+ * right — are unit-testable without ever booting Chromium.
14
+ *
15
+ * Doctrine (binding, docs/WAVE6-DOCTOR-DESIGN.md doctrine header): anti-shim
16
+ * — never fabricate a PASS. Every declared world gets EXACTLY one verdict:
17
+ *
18
+ * - **MOUNTED** — real evidence cleared the bar for this world's route
19
+ * (see {@link DoctorIngestEvidence}/{@link DoctorModuleEvidence}/
20
+ * {@link DoctorReactIngestEvidence} — the last added wave 10 Slice B,
21
+ * docs/WAVE10-REACT-INGEST-DOM-AUTHORING-DESIGN.md D-L3), OR a composite
22
+ * sibling was present in the live sibling-mount list (see
23
+ * {@link DoctorSiblingEvidence} — a deliberately WEAKER, presence-only
24
+ * bar, checked last, never conflated with the ingest-capture bar above).
25
+ * - **FAILED** — a `__vgaiMountFailureReports()` entry named this world;
26
+ * its message is carried verbatim. Checked FIRST, ahead of any evidence
27
+ * map — a throwing mount attempt can leave stale/partial evidence behind
28
+ * (defensive; today's runner never populates both for the same world).
29
+ * - **UNVERIFIABLE** — neither of the above: no proof, but no named
30
+ * failure either. This is the "never silently omitted, never upgraded
31
+ * to MOUNTED" floor AC-A6 requires, not an error state of this module.
32
+ */
33
+
34
+ import type { DataDoctorFinding } from './data-checks';
35
+
36
+ /** One declared world from the manifest — enough identity to label a verdict row. */
37
+ export interface DoctorDeclaredWorld {
38
+ readonly worldId: string;
39
+ /** `ResolvedWorldEntry.kind` (e.g. `'threejs'`/`'pixijs'`/`'react'`). */
40
+ readonly kind: string;
41
+ /** `ResolvedAdapter['identity']` (e.g. `'ingest-three'`/`'module'`/`'default-three'`). */
42
+ readonly identity: string;
43
+ }
44
+
45
+ /**
46
+ * Scraped evidence for an ingest-capture world (`ingest-three`/`ingest-pixi`
47
+ * — the two identities whose healthy `__vgaiIngest`/`__vgaiIngest2D` payload
48
+ * carries `reflected`/`drawCount`). `null` fields mean "the runner found no
49
+ * such signal at all" (e.g. the world never mounted this session) — distinct
50
+ * from a present-but-failing value (e.g. `reflectedCount: 0`), which still
51
+ * means SOME payload was observed, just not enough to prove MOUNTED.
52
+ */
53
+ export interface DoctorIngestEvidence {
54
+ readonly reflectedCount: number | null;
55
+ /** Whether `drawCount()` was sampled twice across an interval and strictly climbed. `null` = no `drawCount` function was found to sample. */
56
+ readonly drawCountClimbed: boolean | null;
57
+ /** `typeof setPaused === 'function'` on the payload. */
58
+ readonly playControlLive: boolean;
59
+ /** True only for a genuinely tier-disabled floor (embed-only: no loop exists to gate) — never a fake no-op standing in for a real capability. */
60
+ readonly playControlTierDisabled: boolean;
61
+ readonly achievedTier: string | null;
62
+ readonly declaredTier: string | null;
63
+ }
64
+
65
+ /** Scraped evidence for a `{ module }`-route world (`__vgaiModule`) — D-A4: presence + achieved/declared tier is the whole bar (no reflected/drawCount concept for a custom adapter). */
66
+ export interface DoctorModuleEvidence {
67
+ readonly achievedTier: string;
68
+ readonly declaredTier: string | null;
69
+ readonly tierShortfall: boolean;
70
+ }
71
+
72
+ /** One `__vgaiMountFailureReports()` entry, trimmed to what the verdict needs. */
73
+ export interface DoctorMountFailure {
74
+ readonly worldId: string;
75
+ readonly message: string;
76
+ }
77
+
78
+ /**
79
+ * Scraped evidence for a react-ingest world's LIVE DOM-authoring surface
80
+ * (`__vgaiIngestReact.domEvidence()` + `setPausedPresent`, wave 10 Slice B,
81
+ * docs/WAVE10-REACT-INGEST-DOM-AUTHORING-DESIGN.md D-L1/D-L2). The MOUNTED
82
+ * bar for this class is {@link clearsReactIngestMountedBar} — ALL four legs,
83
+ * never partial credit (anti-shim, AC-L2/AC-L3).
84
+ */
85
+ export interface DoctorReactIngestEvidence {
86
+ readonly elementCount: number;
87
+ readonly hierarchyNodeCount: number;
88
+ readonly reactRootPresent: boolean;
89
+ readonly setPausedPresent: boolean;
90
+ readonly achievedTier: string | null;
91
+ }
92
+
93
+ /**
94
+ * Scraped evidence for a COMPOSITE SIBLING world (`window.__vgaiSiblingMounts()`,
95
+ * D-H1, docs/WAVE9-SIBLING-PERSISTENCE-DOCTOR-DESIGN.md Slice B) — deliberately
96
+ * a DISTINCT class from {@link DoctorIngestEvidence}/{@link DoctorModuleEvidence}
97
+ * (D-H2): the bar for this evidence is PRESENCE in the live sibling-mount list,
98
+ * not a capture/draw proof. Never conflate this with `clearsIngestMountedBar`'s
99
+ * ingest-capture bar — a sibling's evidence class name says, on its face, that
100
+ * this is the weaker of the two.
101
+ */
102
+ export interface DoctorSiblingEvidence {
103
+ /** `typeof sibling.setPaused === 'function'` — absent for a bare `default-react` sibling (no loop to gate). */
104
+ readonly setPausedPresent: boolean;
105
+ /** `!!sibling.authoring` — present whenever the sibling mounted with a store (every production caller). */
106
+ readonly authoringPresent: boolean;
107
+ /** `sibling.authoring?.capabilities.persist === true` — false/absent authoring never reads as persistable. */
108
+ readonly authoringPersist: boolean;
109
+ }
110
+
111
+ /**
112
+ * Every raw datum the browser-phase runner scraped off the live page —
113
+ * data-in for {@link assembleDoctorReport}. Maps/sets are keyed by worldId so
114
+ * a world with no entry anywhere here means "no signal was ever observed for
115
+ * it" (the UNVERIFIABLE floor, never fabricated as MOUNTED).
116
+ */
117
+ export interface DoctorRawSignals {
118
+ readonly ingestEvidence: ReadonlyMap<string, DoctorIngestEvidence>;
119
+ readonly moduleEvidence: ReadonlyMap<string, DoctorModuleEvidence>;
120
+ /** worldIds observed via `__vgaiIngestReact` — presence-only (D-N8: no authoring/capture surface in v1), deliberately NOT treated as proof of a working mount (see UNVERIFIABLE branch below). */
121
+ readonly reactIngestPresence: ReadonlySet<string>;
122
+ /**
123
+ * Per-worldId composite-sibling evidence (D-H2) — a presence-based bar,
124
+ * checked AFTER ingest/module evidence and react presence, BEFORE the
125
+ * terminal UNVERIFIABLE branch (see {@link assembleDoctorReport}).
126
+ * OPTIONAL (unlike the other maps above) so every pre-D-H2 `DoctorRawSignals`
127
+ * literal (existing tests, any other caller) keeps compiling/running
128
+ * byte-stable — an absent map reads exactly like an empty one.
129
+ */
130
+ readonly siblingEvidence?: ReadonlyMap<string, DoctorSiblingEvidence>;
131
+ /**
132
+ * Per-worldId react-ingest DOM evidence (D-L2, wave 10 Slice B,
133
+ * docs/WAVE10-REACT-INGEST-DOM-AUTHORING-DESIGN.md) — checked in
134
+ * {@link assembleDoctorReport} after ingest/module evidence, before the
135
+ * legacy `reactIngestPresence`-only branch and the sibling branch (which
136
+ * stays LAST). OPTIONAL (unlike `reactIngestPresence`) so every
137
+ * pre-Slice-B `DoctorRawSignals` literal keeps compiling/running
138
+ * byte-stable — an absent map reads exactly like an empty one (the same
139
+ * wave-9B lesson `siblingEvidence` above already applies).
140
+ */
141
+ readonly reactIngestEvidence?: ReadonlyMap<string, DoctorReactIngestEvidence>;
142
+ readonly mountFailures: readonly DoctorMountFailure[];
143
+ }
144
+
145
+ interface DoctorVerdictBase extends DoctorDeclaredWorld {}
146
+
147
+ export interface DoctorMountedVerdict extends DoctorVerdictBase {
148
+ readonly verdict: 'MOUNTED';
149
+ readonly evidence:
150
+ | DoctorIngestEvidence
151
+ | DoctorModuleEvidence
152
+ | DoctorSiblingEvidence
153
+ | DoctorReactIngestEvidence;
154
+ }
155
+
156
+ export interface DoctorFailedVerdict extends DoctorVerdictBase {
157
+ readonly verdict: 'FAILED';
158
+ readonly message: string;
159
+ }
160
+
161
+ export interface DoctorUnverifiableVerdict extends DoctorVerdictBase {
162
+ readonly verdict: 'UNVERIFIABLE';
163
+ readonly reason: string;
164
+ }
165
+
166
+ export type DoctorVerdict = DoctorMountedVerdict | DoctorFailedVerdict | DoctorUnverifiableVerdict;
167
+
168
+ /** True iff an ingest-capture evidence bundle proves a real, live, editable mount (D-A4's MOUNTED bar for ingest worlds). */
169
+ function clearsIngestMountedBar(ev: DoctorIngestEvidence): boolean {
170
+ const reflectedOk = ev.reflectedCount !== null && ev.reflectedCount > 0;
171
+ const drawOk = ev.drawCountClimbed === true;
172
+ const playOk = ev.playControlLive || ev.playControlTierDisabled;
173
+ return reflectedOk && drawOk && playOk;
174
+ }
175
+
176
+ /** Named, honest reason an ingest world's evidence fell short of the MOUNTED bar — never silently downgraded without saying why. */
177
+ function ingestShortfallReason(worldId: string, ev: DoctorIngestEvidence): string {
178
+ return (
179
+ `world "${worldId}" produced an ingest-capture signal but did not clear the MOUNTED bar ` +
180
+ `(reflectedCount=${ev.reflectedCount ?? 'null'}, drawCountClimbed=${ev.drawCountClimbed ?? 'null'}, ` +
181
+ `playControlLive=${ev.playControlLive}, playControlTierDisabled=${ev.playControlTierDisabled}) — ` +
182
+ 'never upgraded to MOUNTED without all three (docs/WAVE6-DOCTOR-DESIGN.md D-A4).'
183
+ );
184
+ }
185
+
186
+ /** True iff react-ingest DOM evidence proves a real, live, editable mount (D-L3's MOUNTED bar for react-ingest worlds) — ALL four legs, never partial credit. */
187
+ function clearsReactIngestMountedBar(ev: DoctorReactIngestEvidence): boolean {
188
+ return (
189
+ ev.elementCount > 0 && ev.hierarchyNodeCount > 0 && ev.reactRootPresent && ev.setPausedPresent
190
+ );
191
+ }
192
+
193
+ /** Named, honest reason a react-ingest world's DOM evidence fell short of the MOUNTED bar — names exactly which leg(s), never silently downgraded without saying why. */
194
+ function reactIngestShortfallReason(worldId: string, ev: DoctorReactIngestEvidence): string {
195
+ const failedLegs: string[] = [];
196
+ if (!(ev.elementCount > 0)) failedLegs.push(`elementCount=${ev.elementCount}`);
197
+ if (!(ev.hierarchyNodeCount > 0)) failedLegs.push(`hierarchyNodeCount=${ev.hierarchyNodeCount}`);
198
+ if (!ev.reactRootPresent) failedLegs.push('reactRootPresent=false');
199
+ if (!ev.setPausedPresent) failedLegs.push('setPausedPresent=false');
200
+ return (
201
+ `world "${worldId}" produced react-ingest DOM evidence but did not clear the MOUNTED bar ` +
202
+ `(${failedLegs.join(', ')}) — never upgraded to MOUNTED without all four legs ` +
203
+ '(docs/WAVE10-REACT-INGEST-DOM-AUTHORING-DESIGN.md D-L3).'
204
+ );
205
+ }
206
+
207
+ /** Verdict for a world with ingest-capture evidence — factored out of {@link assembleDoctorReport} to keep its own cognitive complexity within the repo's lint budget. */
208
+ function verdictForIngestEvidence(
209
+ world: DoctorDeclaredWorld,
210
+ ev: DoctorIngestEvidence,
211
+ ): DoctorVerdict {
212
+ const { worldId, kind, identity } = world;
213
+ return clearsIngestMountedBar(ev)
214
+ ? { worldId, kind, identity, verdict: 'MOUNTED', evidence: ev }
215
+ : {
216
+ worldId,
217
+ kind,
218
+ identity,
219
+ verdict: 'UNVERIFIABLE',
220
+ reason: ingestShortfallReason(worldId, ev),
221
+ };
222
+ }
223
+
224
+ /** Verdict for a world with react-ingest DOM evidence (D-L3) — same factoring reason as {@link verdictForIngestEvidence} above. */
225
+ function verdictForReactIngestEvidence(
226
+ world: DoctorDeclaredWorld,
227
+ ev: DoctorReactIngestEvidence,
228
+ ): DoctorVerdict {
229
+ const { worldId, kind, identity } = world;
230
+ return clearsReactIngestMountedBar(ev)
231
+ ? { worldId, kind, identity, verdict: 'MOUNTED', evidence: ev }
232
+ : {
233
+ worldId,
234
+ kind,
235
+ identity,
236
+ verdict: 'UNVERIFIABLE',
237
+ reason: reactIngestShortfallReason(worldId, ev),
238
+ };
239
+ }
240
+
241
+ /**
242
+ * Assemble exactly one verdict per declared world (AC-A4). Iterates
243
+ * `declaredWorlds` (never the signal maps) so a world with zero matching
244
+ * signals still gets its own UNVERIFIABLE row rather than being dropped.
245
+ */
246
+ export function assembleDoctorReport(
247
+ declaredWorlds: readonly DoctorDeclaredWorld[],
248
+ raw: DoctorRawSignals,
249
+ ): readonly DoctorVerdict[] {
250
+ const failureByWorld = new Map(raw.mountFailures.map((f) => [f.worldId, f.message]));
251
+
252
+ return declaredWorlds.map((world): DoctorVerdict => {
253
+ const { worldId, kind, identity } = world;
254
+
255
+ // FAILED is checked first — a named failure is the strongest, most
256
+ // actionable signal doctor can report, and takes precedence over any
257
+ // (possibly stale/partial) evidence left behind by the same attempt.
258
+ const failureMessage = failureByWorld.get(worldId);
259
+ if (failureMessage !== undefined) {
260
+ return { worldId, kind, identity, verdict: 'FAILED', message: failureMessage };
261
+ }
262
+
263
+ const ingestEv = raw.ingestEvidence.get(worldId);
264
+ if (ingestEv) return verdictForIngestEvidence(world, ingestEv);
265
+
266
+ const moduleEv = raw.moduleEvidence.get(worldId);
267
+ if (moduleEv) {
268
+ return { worldId, kind, identity, verdict: 'MOUNTED', evidence: moduleEv };
269
+ }
270
+
271
+ // D-L3 (wave 10 Slice B, docs/WAVE10-REACT-INGEST-DOM-AUTHORING-DESIGN.md):
272
+ // checked in the SAME position the presence-only D-N8 branch used to
273
+ // occupy — after ingest/module evidence, before the sibling branch
274
+ // (which stays LAST) — so a world whose ingest evidence already fell
275
+ // short above is NEVER reconsidered here, and below-bar react evidence
276
+ // is NEVER reconsidered by the sibling branch below (AC-L5, anti-shim
277
+ // ordering pin: mirrors the ingest-evidence early-return above).
278
+ const reactEv = raw.reactIngestEvidence?.get(worldId);
279
+ if (reactEv) return verdictForReactIngestEvidence(world, reactEv);
280
+
281
+ if (raw.reactIngestPresence.has(worldId)) {
282
+ return {
283
+ worldId,
284
+ kind,
285
+ identity,
286
+ verdict: 'UNVERIFIABLE',
287
+ reason:
288
+ `world "${worldId}" mounted an ingest-react session (__vgaiIngestReact present), but no ` +
289
+ '`domEvidence()` accessor was found on its payload (an older build predating wave 10 ' +
290
+ 'Slice B, docs/WAVE10-REACT-INGEST-DOM-AUTHORING-DESIGN.md D-L1) — presence alone is not ' +
291
+ 'proof of a working mount, so this is reported honestly rather than upgraded to MOUNTED.',
292
+ };
293
+ }
294
+
295
+ // D-H2 (docs/WAVE9-SIBLING-PERSISTENCE-DOCTOR-DESIGN.md Slice B): checked
296
+ // LAST among the "some evidence exists" branches, after ingest/module
297
+ // evidence and react presence — so a world whose ingest evidence exists
298
+ // but fell short of `clearsIngestMountedBar` above is NEVER reconsidered
299
+ // here (AC-H4, anti-shim: sibling evidence never upgrades a below-the-bar
300
+ // ingest world). The bar itself is presence-in-the-live-list — parallel
301
+ // to the module-route bar above — and the evidence class name
302
+ // ({@link DoctorSiblingEvidence}) makes that weaker basis explicit; never
303
+ // conflate it with `clearsIngestMountedBar`'s capture/draw proof.
304
+ const siblingEv = raw.siblingEvidence?.get(worldId);
305
+ if (siblingEv) {
306
+ return { worldId, kind, identity, verdict: 'MOUNTED', evidence: siblingEv };
307
+ }
308
+
309
+ return {
310
+ worldId,
311
+ kind,
312
+ identity,
313
+ verdict: 'UNVERIFIABLE',
314
+ reason:
315
+ `no headless health signal was observed for world "${worldId}" (kind "${kind}", identity ` +
316
+ `"${identity}"), and no mount-failure entry named it either — either this identity has no ` +
317
+ 'headless signal wired in doctor this slice, or the mount never ran at all.',
318
+ };
319
+ });
320
+ }
321
+
322
+ // ---------------------------------------------------------------------------
323
+ // The full `--json` envelope (D-A5) — also pure; the CLI/runner only supply
324
+ // the already-computed static summary + verdict list.
325
+ // ---------------------------------------------------------------------------
326
+
327
+ /** Static (Phase 1) result summary, as far as the JSON report cares (AC-A1). */
328
+ export interface DoctorStaticSummary {
329
+ readonly ok: boolean;
330
+ /** Human-readable issues (manifest errors / missing declared files) — empty when `ok`. */
331
+ readonly issues: readonly string[];
332
+ }
333
+
334
+ /**
335
+ * Bump if this JSON shape ever changes incompatibly — consumers should gate
336
+ * on it. Bumped 1 -> 2 (D-H4, docs/WAVE9-SIBLING-PERSISTENCE-DOCTOR-DESIGN.md
337
+ * Slice B): a `MOUNTED` verdict's `evidence` union gained the
338
+ * {@link DoctorSiblingEvidence} variant. Bumped 2 -> 3 (D-L4, wave 10 Slice B,
339
+ * docs/WAVE10-REACT-INGEST-DOM-AUTHORING-DESIGN.md): the `evidence` union
340
+ * gained the {@link DoctorReactIngestEvidence} variant (a react-ingest world
341
+ * can now MOUNT on real DOM evidence, not just be UNVERIFIABLE). Bumped
342
+ * 3 -> 4 (docs/DATA-TOOLS-DESIGN.md §6.6, W5 enforcement): the envelope
343
+ * gained the optional top-level `data` section (data-asset findings), and an
344
+ * ERROR-severity data finding now flips the exit code (see
345
+ * {@link doctorExitCode}).
346
+ */
347
+ export const DOCTOR_VERSION = 4 as const;
348
+
349
+ export interface DoctorJsonReport {
350
+ readonly doctorVersion: typeof DOCTOR_VERSION;
351
+ readonly static: DoctorStaticSummary;
352
+ readonly worlds: readonly DoctorVerdict[];
353
+ /** Data-asset check findings (docs/DATA-TOOLS-DESIGN.md §6.6) — present iff the caller ran the data scan (run-doctor.ts always does; older/other callers may not). */
354
+ readonly data?: { readonly findings: readonly DataDoctorFinding[] };
355
+ }
356
+
357
+ /** Build the stable `--json` envelope (D-A5: "stable shape, top-level `doctorVersion`, static-phase result included"; the version literal itself is {@link DOCTOR_VERSION}, currently `4`). The `data` section is included only when findings were actually scanned — never fabricated as an empty pass. */
358
+ export function buildDoctorJsonReport(
359
+ staticSummary: DoctorStaticSummary,
360
+ worldVerdicts: readonly DoctorVerdict[],
361
+ dataFindings?: readonly DataDoctorFinding[],
362
+ ): DoctorJsonReport {
363
+ const base: DoctorJsonReport = {
364
+ doctorVersion: DOCTOR_VERSION,
365
+ static: staticSummary,
366
+ worlds: worldVerdicts,
367
+ };
368
+ return dataFindings ? { ...base, data: { findings: dataFindings } } : base;
369
+ }
370
+
371
+ /**
372
+ * D-A5: "Exit 0 iff static passed AND no world is FAILED. Exit 1 otherwise.
373
+ * UNVERIFIABLE does not flip the exit code." Extended by W5 enforcement
374
+ * (docs/DATA-TOOLS-DESIGN.md §6.6): an ERROR-severity data-asset finding
375
+ * (schema failure, dangling ref) also exits 1 — warn/info findings never do,
376
+ * mirroring how UNVERIFIABLE worlds never do.
377
+ */
378
+ export function doctorExitCode(
379
+ staticSummary: DoctorStaticSummary,
380
+ worldVerdicts: readonly DoctorVerdict[],
381
+ dataFindings?: readonly DataDoctorFinding[],
382
+ ): 0 | 1 {
383
+ if (!staticSummary.ok) return 1;
384
+ if (dataFindings?.some((f) => f.severity === 'error')) return 1;
385
+ return worldVerdicts.some((v) => v.verdict === 'FAILED') ? 1 : 0;
386
+ }