@volter/game-editor 0.5.65
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.
- package/BUNDLED_NOTICES +11228 -0
- package/LICENSE +686 -0
- package/LICENSE-BLENDER +1409 -0
- package/LICENSE-MCP +21 -0
- package/NOTICE +21 -0
- package/catalog/entries/audio.json +14 -0
- package/catalog/entries/bake.json +36 -0
- package/catalog/entries/blender.json +17 -0
- package/catalog/entries/react-root.json +24 -0
- package/catalog/entries/reflections.json +29 -0
- package/catalog/entries/static-batch.json +25 -0
- package/catalog/project-source/LICENSE +37 -0
- package/catalog/project-source/src/contributions/bake.kind.ts +40 -0
- package/catalog/project-source/src/contributions/builder-document.tsx +65 -0
- package/catalog/project-source/src/contributions/builder-state.ts +27 -0
- package/catalog/project-source/src/contributions/builder-ui.tsx +61 -0
- package/catalog/project-source/src/contributions/module-source.ts +502 -0
- package/catalog/project-source/src/contributions/pages.finder.ts +61 -0
- package/catalog/project-source/src/lib/audio/sfx.ts +255 -0
- package/catalog/project-source/src/lib/bake/asset-seed.ts +128 -0
- package/catalog/project-source/src/lib/bake/bake-schema.ts +65 -0
- package/catalog/project-source/src/lib/bake/clip-layering.ts +203 -0
- package/catalog/project-source/src/lib/bake/gltf-bake.ts +657 -0
- package/catalog/project-source/src/lib/bake/mesh-slice.ts +157 -0
- package/catalog/project-source/src/lib/bake/node-image.ts +463 -0
- package/catalog/project-source/src/lib/bake/object3d-bake.ts +121 -0
- package/catalog/project-source/src/lib/bake/object3d-lifecycle.ts +15 -0
- package/catalog/project-source/src/lib/bake/object3d-source.ts +8 -0
- package/catalog/project-source/src/lib/bake/params.ts +75 -0
- package/catalog/project-source/src/lib/bake/rest-relative-animation.ts +217 -0
- package/catalog/project-source/src/lib/bake/use-model.ts +95 -0
- package/catalog/project-source/src/lib/react-root/index.ts +20 -0
- package/catalog/project-source/src/lib/react-root/react-root-adapter.tsx +57 -0
- package/catalog/project-source/src/lib/reflections/ReflectionProbe.tsx +201 -0
- package/catalog/project-source/src/lib/reflections/index.ts +19 -0
- package/catalog/project-source/src/lib/reflections/probe-controller.ts +75 -0
- package/catalog/project-source/src/lib/reflections/reflection-probe-registry.ts +402 -0
- package/catalog/project-source/src/lib/reflections/volume-reflection-material.ts +416 -0
- package/catalog/project-source/src/lib/static-batch/Frozen.tsx +91 -0
- package/catalog/project-source/src/lib/static-batch/freeze.ts +579 -0
- package/catalog/project-source/src/lib/static-batch/index.ts +44 -0
- package/catalog/project-source/src/lib/static-batch/mutation-watch.ts +149 -0
- package/catalog/project-source/src/lib/static-batch/shadow-cache.ts +171 -0
- package/catalog/project-source/src/tools/bake-module.tool.ts +139 -0
- package/catalog/project-source/src/tools/bake-preview.tool.ts +475 -0
- package/dist/.vite/manifest.json +8304 -0
- package/dist/assets/AssetBudgetPanel-J9--TZPU.js +23 -0
- package/dist/assets/AudioViewer-CR7333fe.js +1 -0
- package/dist/assets/BVHLoader-6bQIXhMX.js +1 -0
- package/dist/assets/BufferGeometryUtils-Nz37EVXf.js +1 -0
- package/dist/assets/CCDIKSolver-CD7vLpsK.js +1 -0
- package/dist/assets/CSS3DRenderer-CL7ABLVK.js +1 -0
- package/dist/assets/CanvasSceneViewport-Bpi6gVTC.js +1 -0
- package/dist/assets/ColladaLoader-aVOPIsCh.js +3 -0
- package/dist/assets/ColorInput-DQBNrW-K.js +1 -0
- package/dist/assets/ConvexGeometry-CAzXlPVG.js +1 -0
- package/dist/assets/ConvexHull-BINCESFp.js +1 -0
- package/dist/assets/CopyShader-BzTUYzf6.js +24 -0
- package/dist/assets/DecalGeometry-160KRdfg.js +1 -0
- package/dist/assets/EXRLoader-Cdans_oJ.js +1 -0
- package/dist/assets/EffectComposer-DQxfsXBZ.js +1 -0
- package/dist/assets/EntityModelDocument-Ccvw66N9.js +1 -0
- package/dist/assets/EnvironmentAssetDocument-Dg-Qq6O6.js +1 -0
- package/dist/assets/FBXLoader-B4MoUtAn.js +1 -0
- package/dist/assets/Fbo-Dz-Soul3.js +1 -0
- package/dist/assets/FontLoader-CvGoVI5a.js +2 -0
- package/dist/assets/GLTFExporter-HgrCle4L.js +1 -0
- package/dist/assets/GameplaySessionTimeline-B52W3FVZ.js +1 -0
- package/dist/assets/Gltf-UHVWKz65.js +2 -0
- package/dist/assets/HDRLoader-THU88oVD.js +5 -0
- package/dist/assets/ImageViewer-Tn7s_MnW.js +1 -0
- package/dist/assets/InspectorCanvasPreview-CEr_Kxna.js +1 -0
- package/dist/assets/InspectorObjectPreview-CVejvLRg.js +1 -0
- package/dist/assets/JsonAssetDocument-C7VGcLhu.js +3 -0
- package/dist/assets/LineSegments2-CsOpKSiM.js +370 -0
- package/dist/assets/MTLLoader-_Xzd4_0p.js +2 -0
- package/dist/assets/MarchingCubes-Bsp4wrzd.js +1 -0
- package/dist/assets/MeshBVH-BiDHDcu-.js +1 -0
- package/dist/assets/ModelAssetDocument-F5GACirk.js +1 -0
- package/dist/assets/NonThreeAuthoringBootstrap-C3woOMxX.js +1 -0
- package/dist/assets/OBJLoader-BhIC6Hwo.js +5 -0
- package/dist/assets/Object3DDocumentViewport-DB7T0NTl.js +2 -0
- package/dist/assets/Object3DPreview-DL8oi7Xs.js +2 -0
- package/dist/assets/OnlineAssetDetail-C6z9RvYK.js +2 -0
- package/dist/assets/OrbitControls-CM3bPray.js +1 -0
- package/dist/assets/OrbitControls-DLpayAaK.js +1 -0
- package/dist/assets/OrthographicCamera-bQozawrh.js +1 -0
- package/dist/assets/PLYLoader-DYQqs5Ik.js +2 -0
- package/dist/assets/PerspectiveCamera-CQ2lh01o.js +1 -0
- package/dist/assets/PlayBar-BR-Vg3Jb.js +2 -0
- package/dist/assets/RGBELoader-BgBcQv8j.js +1 -0
- package/dist/assets/RectAreaLightUniformsLib-CMMoWfll.js +1 -0
- package/dist/assets/RenderPass-DPrn-Uky.js +1 -0
- package/dist/assets/ResolutionPicker-C67vs1F3.js +1 -0
- package/dist/assets/RootSelectionOverlay-BZPlxmXU.js +1 -0
- package/dist/assets/RoundedBox-BDGBg6QZ.js +1 -0
- package/dist/assets/RoundedBoxGeometry-CyUuYPfW.js +1 -0
- package/dist/assets/STLLoader-Dyv3lsxY.js +1 -0
- package/dist/assets/SelectionBox-D_75KrNW.js +1 -0
- package/dist/assets/ShaderPass-QYRpPuFJ.js +1 -0
- package/dist/assets/ShadowEditor-DkImwu1Y.js +1 -0
- package/dist/assets/SimplifyModifier-C8ayvRzd.js +1 -0
- package/dist/assets/SkeletonUtils-Cps8GOC6.js +1 -0
- package/dist/assets/Sky-CQKCgYDs.js +154 -0
- package/dist/assets/SourceAssetViewer-2SWQoUut.js +12 -0
- package/dist/assets/StageHost-BN-IoTef.js +2 -0
- package/dist/assets/StoryPreviewMount-DjyxRCXu.js +1 -0
- package/dist/assets/TDSLoader-D_T8XJ81.js +1 -0
- package/dist/assets/TextGeometry-DOWAEUV0.js +1 -0
- package/dist/assets/ThreeBoardDocument-CVncZyIq.js +2 -0
- package/dist/assets/ThreeIsolationSceneContent-C8C8whSF.js +1 -0
- package/dist/assets/ToolObject3DPreview-BRoDWe4Q.js +1 -0
- package/dist/assets/TrackballControls-DfznuQTh.js +1 -0
- package/dist/assets/TransientHint-BB1fwrvK.js +1 -0
- package/dist/assets/UnrealBloomPass-CR-cS_K5.js +83 -0
- package/dist/assets/VideoViewer-C6kocgIU.js +1 -0
- package/dist/assets/_commonjsHelpers-D6-XlEtG.js +1 -0
- package/dist/assets/active-ingest-CT6jSGhr.js +3 -0
- package/dist/assets/adapter-surface-Y37Nzcyw.js +1 -0
- package/dist/assets/after-paint-CB4bz1-e.js +1 -0
- package/dist/assets/agx-base-srgb-BjWZJgrZ.lut +0 -0
- package/dist/assets/agx-look-medium-high-contrast-D5F_ob1L.lut +0 -0
- package/dist/assets/agx-look-punchy-dxsnycNs.lut +0 -0
- package/dist/assets/animation-clock-CnjZqfJp.js +1 -0
- package/dist/assets/asset-budget-asset.menu-Di3KiQCd.js +1 -0
- package/dist/assets/asset-budget.action-DCzzcuTh.js +1 -0
- package/dist/assets/asset-budget.document-BWU8wnVW.js +1 -0
- package/dist/assets/asset-budget.menu-BGZpT_31.js +1 -0
- package/dist/assets/asset-editor-persistence-BBaONOJt.js +1 -0
- package/dist/assets/asset-loaders-BOy5Ss8k.js +2 -0
- package/dist/assets/asset-parse-error-BHMwBRU3.js +3 -0
- package/dist/assets/assets-BuSoGjBX.js +1 -0
- package/dist/assets/audio-meter-CyiToeUW.js +1 -0
- package/dist/assets/audio-unlock.service-CfUgUQVy.js +1 -0
- package/dist/assets/audio.utility-C6K6jem8.js +1 -0
- package/dist/assets/authoring-CgcOFhGS.js +1 -0
- package/dist/assets/autoplay.service-BZBGNSvt.js +1 -0
- package/dist/assets/axe-LkjAB6Lv.js +30 -0
- package/dist/assets/babylon-authoring-adapter-CL02gWDn.js +1 -0
- package/dist/assets/binding-Bmxqznjk.js +1 -0
- package/dist/assets/binding-identity-DcTpbvyg.js +1 -0
- package/dist/assets/binding-resolver-Dsh-6Vor.js +2 -0
- package/dist/assets/blender-emscripten-engine-9ZIRYNhe.js +2 -0
- package/dist/assets/blender-node-editor.document-iIDkB9sI.js +1 -0
- package/dist/assets/blender-properties-tab-C5CyPydi.js +3 -0
- package/dist/assets/blender-runtime-host-BpBwGv7r.js +4 -0
- package/dist/assets/blender-runtime-skin-DyQ9B6CR.js +7140 -0
- package/dist/assets/blender-runtime.document-lIYhhPtv.js +8 -0
- package/dist/assets/blender-timeline.document-77fx96N7.js +1 -0
- package/dist/assets/blender-uv-editor.document-fsT7B4gV.js +1 -0
- package/dist/assets/blender-version.status-o6dBm3n6.js +1 -0
- package/dist/assets/blender-wali-engine-wTOcrOMC.js +2 -0
- package/dist/assets/blender.command-BtJeWk-A.js +1 -0
- package/dist/assets/blender.keymap-GAQgeklK.js +1 -0
- package/dist/assets/blender.status-ujwGU6dU.js +1 -0
- package/dist/assets/blender.style-Dcem5f_T.js +1 -0
- package/dist/assets/board-guides-DziKSTss.js +1 -0
- package/dist/assets/body-marks-h_xDxiDE.js +1 -0
- package/dist/assets/bridge.command-o5gDNEwa.js +3 -0
- package/dist/assets/browserAll-DeWblVez.js +1 -0
- package/dist/assets/build-profiles.document-kW8iw8Xj.js +2 -0
- package/dist/assets/build-progress.status-BNRLytg2.js +1 -0
- package/dist/assets/build-session-C_sg3CEn.js +9 -0
- package/dist/assets/build.action-lYjTLb7K.js +1 -0
- package/dist/assets/build.command-DIGgUPU_.js +1 -0
- package/dist/assets/build.header-CKajx5U3.js +1 -0
- package/dist/assets/build.menu-BdU0QXny.js +1 -0
- package/dist/assets/build.service-CbBAyeu3.js +1 -0
- package/dist/assets/bus-mixer-CGfWDP3C.js +1 -0
- package/dist/assets/camera-KEsKvOL7.js +1 -0
- package/dist/assets/camera-runtime.inspector-B7unX4g1.js +1 -0
- package/dist/assets/canvas-entry-runtime-SfBxaR9M.js +2 -0
- package/dist/assets/capability-coverage-Czas4C18.js +7 -0
- package/dist/assets/chrome-trace-YZHMz60-.js +1 -0
- package/dist/assets/client-DNXj_opT.js +25 -0
- package/dist/assets/clip-planes-CnAI6DZn.js +1 -0
- package/dist/assets/collaboration-presence-DLoZ7EA3.js +1 -0
- package/dist/assets/collider-Bw0mXlsW.js +1 -0
- package/dist/assets/collider-dimensions-9xoFADXN.js +1 -0
- package/dist/assets/collision-system-DpNUJRJ3.js +1 -0
- package/dist/assets/command-results-CUQD49AB.js +1 -0
- package/dist/assets/component-board-registry-DrIa2csL.js +1 -0
- package/dist/assets/component-board.service-DCwcUssB.js +2 -0
- package/dist/assets/component-board.service-DidCyM4O.js +2 -0
- package/dist/assets/component-verbs.command-7xzfjGMZ.js +1 -0
- package/dist/assets/component-verbs.service-Bv9jId3q.js +1 -0
- package/dist/assets/config-CEZk921y.js +6 -0
- package/dist/assets/configurations-DTBMdaTL.js +1 -0
- package/dist/assets/connection.status-BifnQdOs.js +1 -0
- package/dist/assets/console-bridge-CHcb-xk5.js +1 -0
- package/dist/assets/constraint-De8f1Uiq.js +1 -0
- package/dist/assets/constraints.inspector-B9U9Yj-u.js +1 -0
- package/dist/assets/countdown-timer-CUm74dom.js +1 -0
- package/dist/assets/coverage.service-VTX5Mcsx.js +3 -0
- package/dist/assets/crash-null-boundary-v3a0uEgw.js +1 -0
- package/dist/assets/create-runtime-CVCc1BDx.js +1 -0
- package/dist/assets/curve-7Q79VgU4.js +1 -0
- package/dist/assets/data-asset-4hjS642k.js +5 -0
- package/dist/assets/data-check-core-CQEjuFyt.js +1 -0
- package/dist/assets/data-ref-CQI28C0D.js +1 -0
- package/dist/assets/debug-bridge-CCohG-d-.js +1 -0
- package/dist/assets/debug-registry-Dgvitx8M.js +1 -0
- package/dist/assets/defaults-DKiBJCPN.js +1 -0
- package/dist/assets/deferred-commands-CEMmvL47.js +1 -0
- package/dist/assets/deferred-ingest-play-DCJi_Pgu.js +2 -0
- package/dist/assets/design-time-layers-wYgOTgni.js +1 -0
- package/dist/assets/design-time-mount-registry-DUpXCg1U.js +1 -0
- package/dist/assets/design-time-mount.service-BzY21e1F.js +2 -0
- package/dist/assets/design-time-react-mount-DM9WLrF2.js +1 -0
- package/dist/assets/dev-build-gDYrq68M.js +1 -0
- package/dist/assets/directional-shadow-fit-BVkOKAv2.js +1 -0
- package/dist/assets/document-preview-three-Kbg2GCLm.js +1 -0
- package/dist/assets/dom-authoring-adapter-CY8C7f4T.js +1 -0
- package/dist/assets/draco3d-BBUTcNkz.js +1 -0
- package/dist/assets/draco_decoder-DsQ12WqX.wasm +0 -0
- package/dist/assets/draco_encoder-2Tcju8Ns.wasm +0 -0
- package/dist/assets/edit-mode-audio.service-Bzetwd5B.js +1 -0
- package/dist/assets/edit-mode-authoring-B47TfLqu.js +1 -0
- package/dist/assets/edit-mode-networking.service-YCX6RRb_.js +1 -0
- package/dist/assets/editor-api-BoJIcyZG.js +1 -0
- package/dist/assets/editor-port-C_FeBdeV.js +1 -0
- package/dist/assets/editor-presentation-activity-C7SNSmTd.js +1 -0
- package/dist/assets/entry-adjudication-Cq8Yeb74.js +1 -0
- package/dist/assets/environment-capture-7himRh2L.js +1 -0
- package/dist/assets/esbuild-BHljloGq.wasm +0 -0
- package/dist/assets/events-156d8d12.esm-ErOhDXXB.js +36 -0
- package/dist/assets/extend-B-eOccUm.js +36 -0
- package/dist/assets/extends-CF3RwP-h.js +1 -0
- package/dist/assets/eyedropper-session-bjPXnfeX.js +1 -0
- package/dist/assets/fflate.module-DPuTF8aG.js +6 -0
- package/dist/assets/filmic-srgb-Ct35FIH6.lut +0 -0
- package/dist/assets/first-party-audio-system-DxnGJOER.js +1 -0
- package/dist/assets/first-party-navigation-system-CL5Bquub.js +1 -0
- package/dist/assets/fork-menu-BoVyw2Wu.js +1 -0
- package/dist/assets/frame-debugger-store-D1Mf_5up.js +1 -0
- package/dist/assets/frame-delta-D2nsw_Ht.js +1 -0
- package/dist/assets/frame-pacing-D5acoOzO.js +1 -0
- package/dist/assets/frame-selector-cache-cCeUbcjG.js +1 -0
- package/dist/assets/game-DHOZVb-7.js +1 -0
- package/dist/assets/game-contract-seams-a5YXejOu.js +1 -0
- package/dist/assets/game-css-scope-transform-Eng4wS8E.js +33 -0
- package/dist/assets/game-document.service-DbRfx2V0.js +1 -0
- package/dist/assets/game-eval.command-D-7Dxbj_.js +2 -0
- package/dist/assets/game-input-seams-CIQdetRw.js +1 -0
- package/dist/assets/game-loop-CZvXlY00.js +1 -0
- package/dist/assets/game-module-access-B2mFxv-i.js +1 -0
- package/dist/assets/game.layout-CBASrvQu.js +1 -0
- package/dist/assets/gameplay-export-BoKXm51x.js +1 -0
- package/dist/assets/gameplay-replay-e5umMlXv.js +1 -0
- package/dist/assets/gameplay-rng-trap-CTNhm-UW.js +3 -0
- package/dist/assets/gameplay.command-DxcVYX6F.js +1 -0
- package/dist/assets/generation-DhwCUHPf.css +1 -0
- package/dist/assets/generation-jobs-BQrXbGp_.js +1 -0
- package/dist/assets/generation.service-CDIgObuw.js +1 -0
- package/dist/assets/generations-DzUogZsM.js +1 -0
- package/dist/assets/generations.status-CgOUBCum.js +1 -0
- package/dist/assets/hierarchy-marks-BLPfSIym.js +1 -0
- package/dist/assets/host-pointer-DbGSCjE9.js +1 -0
- package/dist/assets/hosted-asset-materialization-CiIvWkGU.js +2 -0
- package/dist/assets/ibl-override-material-BlmCxFKq.js +6 -0
- package/dist/assets/index-96FBF1Oj.js +1 -0
- package/dist/assets/index-BAWkB0dv.js +1 -0
- package/dist/assets/index-BDnS2b6S.js +1 -0
- package/dist/assets/index-BHp07vN0.js +2 -0
- package/dist/assets/index-BMxv2MNV.js +1 -0
- package/dist/assets/index-BP6wv2A0.js +5 -0
- package/dist/assets/index-BcCBqgM_.js +952 -0
- package/dist/assets/index-Bge9JUr6.js +1 -0
- package/dist/assets/index-BjTCPEQ7.js +134 -0
- package/dist/assets/index-BncK9THk.js +1 -0
- package/dist/assets/index-CJbYy-CF.js +3 -0
- package/dist/assets/index-CVCb_ImQ.js +1 -0
- package/dist/assets/index-CZd9o7Lu.js +1 -0
- package/dist/assets/index-CanA16PU.js +2 -0
- package/dist/assets/index-CinJrH5C.js +9 -0
- package/dist/assets/index-CtzuWnCS.js +1 -0
- package/dist/assets/index-D2fNXpHa.css +1 -0
- package/dist/assets/index-DUpHH6JR.js +3120 -0
- package/dist/assets/index-DUqP1RMl.js +1 -0
- package/dist/assets/index-D_NuPfjr.js +9 -0
- package/dist/assets/index-Da9JhEvW.js +25 -0
- package/dist/assets/index-DgapqSPi.js +7 -0
- package/dist/assets/index-FUuS4qwW.js +562 -0
- package/dist/assets/index-GIL7oGEO.js +4894 -0
- package/dist/assets/index-IEjydI9d.js +1 -0
- package/dist/assets/index-OrYXPk3Z.js +487 -0
- package/dist/assets/index-V1R-e5Bd.js +1 -0
- package/dist/assets/index-bQE2NA0D.js +1 -0
- package/dist/assets/index-gHZae-lw.js +13 -0
- package/dist/assets/index-mjWQmGhM.js +1 -0
- package/dist/assets/index-nmcBqr7Y.js +1 -0
- package/dist/assets/inference-diagnostics-Dhmtfbq_.js +1 -0
- package/dist/assets/ingest-BbYMC2vQ.js +1 -0
- package/dist/assets/ingest-boot-viewport-D5rI_Kk_.js +1 -0
- package/dist/assets/ingest-siblings-Bop2kqVl.js +1 -0
- package/dist/assets/ingest-source-persistence-C_6Hiyra.js +1 -0
- package/dist/assets/ingest.service-BTDnQfM1.js +1 -0
- package/dist/assets/input-manager-DH_hGW6d.js +1 -0
- package/dist/assets/input-router-BNQvtIqw.js +1 -0
- package/dist/assets/inspect-BZfi3mmo.js +1 -0
- package/dist/assets/inspection-CObLhnrs.js +1 -0
- package/dist/assets/instance-fork-actions-D3sRzS3K.js +1 -0
- package/dist/assets/instances.command-D6UkZNO8.js +1 -0
- package/dist/assets/instruments-CKR02Y8I.js +1 -0
- package/dist/assets/json-history-resource-BbxllWfV.js +1 -0
- package/dist/assets/jsx-runtime-DPjLkJKQ.js +9 -0
- package/dist/assets/light-D-_wsKTq.js +1 -0
- package/dist/assets/light-camera-factory-CWbbe9gI.js +1 -0
- package/dist/assets/lil-gui.module.min-BF55BEwA.js +10 -0
- package/dist/assets/live-behaviors-Bxq49Maz.js +1 -0
- package/dist/assets/live-gesture-lock-DJbrjOqf.js +1 -0
- package/dist/assets/loader-CaF_I7Yt.js +1 -0
- package/dist/assets/logger-Dqis-7W1.js +1 -0
- package/dist/assets/loop-gate-report-N52EWFGD.js +1 -0
- package/dist/assets/matcap-texture-CVfalpWT.js +1 -0
- package/dist/assets/material-CpAdjDxn.js +1 -0
- package/dist/assets/material-factory-Bz6h__6g.js +1 -0
- package/dist/assets/mesh-B3WPtNKO.js +1 -0
- package/dist/assets/mesh-shadow-CZnXrw_l.js +1 -0
- package/dist/assets/mikktspace.module-DO88SLib.js +1 -0
- package/dist/assets/model-asset-sections-CFvvihfx.css +1 -0
- package/dist/assets/model-asset-sections.service-CHhFS3ZX.js +1 -0
- package/dist/assets/model.layout-BJUEQYUI.js +1 -0
- package/dist/assets/models.finder-G2jQgIRu.js +1 -0
- package/dist/assets/module-mode-DVBLT9d-.js +2 -0
- package/dist/assets/mount-canvas-ingest-root-DDuAWf_C.js +2 -0
- package/dist/assets/mount-dom-ingest-root-D_EiBgpn.js +2 -0
- package/dist/assets/mount-game-6t9cKNo4.js +1 -0
- package/dist/assets/mount-ingest-root-glv2SSWf.js +2 -0
- package/dist/assets/mount-manifest-CGVxhmAw.js +1 -0
- package/dist/assets/mount-readiness-Ds0HvvLH.js +4 -0
- package/dist/assets/mount-three-ingest-root-hrUt9VVU.js +1 -0
- package/dist/assets/mounted-authoring-Dmg5xmhP.js +1 -0
- package/dist/assets/mounted-story-viewport-source-DjP6RA9U.js +1 -0
- package/dist/assets/native-debug-module-CWicoSGD.js +1 -0
- package/dist/assets/native-entry-surface-BE8LJNdu.js +1 -0
- package/dist/assets/navigation-CIiCx-SM.js +1 -0
- package/dist/assets/navmesh-workflow-store-Cmnm57TB.js +1 -0
- package/dist/assets/navmesh.menu-CAKiH8PW.js +1 -0
- package/dist/assets/navmesh.service-jgDWlgj1.js +1 -0
- package/dist/assets/network.utility-CEu5nHf7.js +1 -0
- package/dist/assets/object-marks-DwvIEBOK.js +1 -0
- package/dist/assets/object3d-authoring-subject-CH5sI2oP.js +1 -0
- package/dist/assets/output-DK8waum9.js +195 -0
- package/dist/assets/particles-Y626p22p.js +1 -0
- package/dist/assets/particles-factory-DnLOHXT8.js +1 -0
- package/dist/assets/pasteboard-materialize-BqSS4QzV.js +20 -0
- package/dist/assets/pasteboard.action-DU3UHtJG.js +2 -0
- package/dist/assets/performance-profiler-D_Jey87L.js +1 -0
- package/dist/assets/physics-registry-B2Jkp8e6.js +1 -0
- package/dist/assets/physics-registry-CUB9cT9p.js +1 -0
- package/dist/assets/pixi-authoring-adapter-CjibysC3.js +1 -0
- package/dist/assets/pixi-live-write-target-DuWydlWb.js +1 -0
- package/dist/assets/pixi-primitive-DlWbUPN9.js +1 -0
- package/dist/assets/pixi-react-root-factory-DYsznYRQ.js +1 -0
- package/dist/assets/pixi-story-model-DNkfnWUV.js +1 -0
- package/dist/assets/pixi-transform-channels-VjdohT11.js +1 -0
- package/dist/assets/play-mode-BFU2PCkG.js +22 -0
- package/dist/assets/play.command-BpqqLqkq.js +2 -0
- package/dist/assets/playtest-BXIL1JEF.js +1 -0
- package/dist/assets/pose-guard-6Osb2cFu.js +1 -0
- package/dist/assets/presentation-BPP5pXeQ.js +1 -0
- package/dist/assets/product-DFhyARJj.js +1 -0
- package/dist/assets/profiler.action-CaXJDp3t.js +1 -0
- package/dist/assets/profiler.menu-BIMF6Bi4.js +1 -0
- package/dist/assets/profiler.utility-BExw9wBO.js +1 -0
- package/dist/assets/project-authoring-session-Bnt6icLr.js +2 -0
- package/dist/assets/project-provenance-BoP3nrle.js +1 -0
- package/dist/assets/prompt-labels-DSMdM2_B.js +1 -0
- package/dist/assets/properties-bone-constraints.inspector-ByT6p7S-.js +1 -0
- package/dist/assets/properties-bone.inspector-z4qJwKO5.js +1 -0
- package/dist/assets/properties-collection.inspector-D0_3ijWU.js +1 -0
- package/dist/assets/properties-constraints.inspector-CESzjuls.js +1 -0
- package/dist/assets/properties-data.inspector-hG8Wgyzb.js +1 -0
- package/dist/assets/properties-material.inspector-CzY0JCQ6.js +1 -0
- package/dist/assets/properties-modifiers.inspector-3dDhwf7g.js +1 -0
- package/dist/assets/properties-object.inspector-CiarfxkT.js +1 -0
- package/dist/assets/properties-output.inspector-DKJxmKSD.js +1 -0
- package/dist/assets/properties-particles.inspector-CCEdYWGm.js +1 -0
- package/dist/assets/properties-physics.inspector-B2c0KUyA.js +1 -0
- package/dist/assets/properties-render.inspector-Bcm0HLRW.js +1 -0
- package/dist/assets/properties-scene.inspector-PIM_eTb_.js +1 -0
- package/dist/assets/properties-texture.inspector-CgFlnyr8.js +1 -0
- package/dist/assets/properties-view-layer.inspector-DGjF_Aqo.js +1 -0
- package/dist/assets/properties-world.inspector-CrMvaRvB.js +1 -0
- package/dist/assets/r3f-design-session-D-4yyDO9.js +2 -0
- package/dist/assets/r3f-physics-binding-BBo8oma9.js +1 -0
- package/dist/assets/r3f-root-factory-z9llD1OU.js +1 -0
- package/dist/assets/r3f-source-authoring-adapter-w6Bc60K-.js +13 -0
- package/dist/assets/rapier-physics-adapter-CaZqWLu0.js +1 -0
- package/dist/assets/rapier-physics-bridge-CMoeVo3Y.js +1 -0
- package/dist/assets/react-18-By2b4xR9.js +1 -0
- package/dist/assets/react-inspector.service-B_neRnSK.js +1 -0
- package/dist/assets/react-root-CEz5NTck.js +1 -0
- package/dist/assets/react-story-board-BDDwiE2H.js +1 -0
- package/dist/assets/react-three-fiber.esm-CycrbDlA.js +1 -0
- package/dist/assets/react-three-rapier.esm-CEfKpvxT.js +1 -0
- package/dist/assets/react-world-authoring-adapter-DmrwM3py.js +1 -0
- package/dist/assets/realm-services-CobQsq_X.js +1 -0
- package/dist/assets/rebind-controller-Dtq19Vb3.js +1 -0
- package/dist/assets/recast-navigation.wasm-compat-Dsoa_MD-.js +1 -0
- package/dist/assets/reflection-probe-Don-M3Fj.js +1 -0
- package/dist/assets/reflection-probe-capture.inspector-CmFUoJCx.js +1 -0
- package/dist/assets/register-render-vitals-BiZk29b2.js +1 -0
- package/dist/assets/registry-02YZjMNp.js +1 -0
- package/dist/assets/relative-path-guard-GiM7Lsz8.js +1 -0
- package/dist/assets/render-audio-control-vMRlePWb.js +1 -0
- package/dist/assets/render-batch-system-0vq7p0VC.js +1 -0
- package/dist/assets/render-census-BrQX_CXv.js +1 -0
- package/dist/assets/render-control-Df3yTiyr.js +1 -0
- package/dist/assets/render-env-D2IV2x5k.js +1 -0
- package/dist/assets/render-features-CPQ_e568.js +1 -0
- package/dist/assets/render-seed-DF4JHy7P.js +1 -0
- package/dist/assets/render-settings-Bw4W9SYF.js +1 -0
- package/dist/assets/render-vitals-BZ5HtvMi.js +1 -0
- package/dist/assets/renderer-config-BqG4O8i9.js +1 -0
- package/dist/assets/resolve-three-SZE_WWUM.js +702 -0
- package/dist/assets/root-seam-contract-Z0VWX3Bk.js +1 -0
- package/dist/assets/rrweb-Cd2ifIRP.js +142 -0
- package/dist/assets/run-ticks-settled-C4ksViyA.js +1 -0
- package/dist/assets/runtime-inspection-l2iFQSsg.js +1 -0
- package/dist/assets/scene-capture-BQKzU0NT.js +1 -0
- package/dist/assets/scene-capture-CGYF-Viw.js +1 -0
- package/dist/assets/scene-document.service-qkDjUrlP.js +2 -0
- package/dist/assets/scene-framing-Cat0GtyJ.js +1 -0
- package/dist/assets/scene-index-B6BZOHMk.js +1 -0
- package/dist/assets/scene-query-C7_N-TpI.js +1 -0
- package/dist/assets/schema-kl9-2B_J.js +1 -0
- package/dist/assets/sculpt.layout-zJF55yCR.js +1 -0
- package/dist/assets/seeded-random-i3mtgBKZ.js +1 -0
- package/dist/assets/session-CZ5JPg-z.js +4877 -0
- package/dist/assets/setup-audio-S8GQF91f.js +1 -0
- package/dist/assets/setup-particles-GCXlnkj_.js +1 -0
- package/dist/assets/setup-physics-BeZ7-LKD.js +1 -0
- package/dist/assets/setup-renderer-DVXaSGin.js +1587 -0
- package/dist/assets/shading.layout-DULYZV6M.js +1 -0
- package/dist/assets/sim-clock-CLflx7kg.js +1 -0
- package/dist/assets/sky-precompute-worker-ChN7J87v.js +1 -0
- package/dist/assets/soft-particle-depth-DGvfQDi1.js +1 -0
- package/dist/assets/source-object3d-authoring-adapter-B-L6Ha6f.js +1 -0
- package/dist/assets/source-persistence-backend-C-aR-B_Y.js +1 -0
- package/dist/assets/spark-renderer-lifecycle-ynWESJcv.js +1 -0
- package/dist/assets/spark.module-4pXQoz5d.js +12120 -0
- package/dist/assets/stage-invalidation-DBg3C3Ta.js +1 -0
- package/dist/assets/stage-keyboard-n-5kKH-m.js +1 -0
- package/dist/assets/stage-overlay-set-B9zp2sLB.js +1 -0
- package/dist/assets/stage-presence-markers-DiGI6A45.js +1 -0
- package/dist/assets/standard-viewport-dressing-CzjAe_0A.js +7 -0
- package/dist/assets/state-bridge-DLP1jLRW.js +1 -0
- package/dist/assets/state-watch.action-BOvQR2qt.js +1 -0
- package/dist/assets/state-watch.menu-CNLvDeeY.js +1 -0
- package/dist/assets/state-watch.utility-Bg1tTTQU.js +1 -0
- package/dist/assets/static-batch-advisor-B13QEH7d.js +1 -0
- package/dist/assets/stats.module-BL02l9NC.js +1 -0
- package/dist/assets/story-arg-descriptors-Ek86ExX7.js +1 -0
- package/dist/assets/story-capture-_gC8dXV-.js +1 -0
- package/dist/assets/story-documents--TDLaPeL.js +3 -0
- package/dist/assets/story-documents.service-BiS_YbNQ.js +1 -0
- package/dist/assets/story-documents.service-CxOPYJ2j.js +1 -0
- package/dist/assets/story-media-presence-BHzk2IV5.js +1 -0
- package/dist/assets/story-mount-turn-DFRTj5hi.js +1 -0
- package/dist/assets/story-pixi-preview-BUrOJZXS.js +1 -0
- package/dist/assets/story-presentation-j2AVN-mK.js +1 -0
- package/dist/assets/story-three-preview-BHEDZzcl.js +2 -0
- package/dist/assets/structural-ids-Dc-7rEwi.js +1 -0
- package/dist/assets/structural-signature-0w-i_ats.js +1 -0
- package/dist/assets/surface-content-Dg9Kkazf.js +1 -0
- package/dist/assets/surface-state-DaxyruCj.js +3 -0
- package/dist/assets/system-adapters-rIC8FSGP.js +1 -0
- package/dist/assets/system-runner-DkOjxEBt.js +1 -0
- package/dist/assets/system-slot-E2mEdiih.js +1 -0
- package/dist/assets/team-playtest.service-DqOinFVA.js +1 -0
- package/dist/assets/texture.layout-DASpHVht.js +1 -0
- package/dist/assets/three-CDSueJyJ.js +1 -0
- package/dist/assets/three-authoring-adapter-CmSQ3Vui.js +1 -0
- package/dist/assets/three-authoring.service-iuHLwtf1.js +2 -0
- package/dist/assets/three-ingest-runtime-QzDYk6tU.js +1 -0
- package/dist/assets/three-projection-core-BmKMZ-nf.js +1 -0
- package/dist/assets/three-viewport-presentation-QBQpSdMh.js +1 -0
- package/dist/assets/three.module-D396a9Mn.js +3840 -0
- package/dist/assets/three.quarks.esm-B1C61jGo.js +1039 -0
- package/dist/assets/tools-B_NJsk05.js +1 -0
- package/dist/assets/transform-writer-BhBBTx33.js +1 -0
- package/dist/assets/transport.header-B-6HQlU2.js +1 -0
- package/dist/assets/trigger-volume-BNK2XzPL.js +1 -0
- package/dist/assets/tuples-CIQ4Bn1U.js +1 -0
- package/dist/assets/tween.module-DciWpAG6.js +1 -0
- package/dist/assets/ui-board-document-Bwtt--xp.js +2 -0
- package/dist/assets/unexported-game-trap-Dm1JYKHy.js +2 -0
- package/dist/assets/unmanaged-root-detector-B8CpbkSN.js +2 -0
- package/dist/assets/upstream-pin-BdgRH6ID.js +1 -0
- package/dist/assets/use-data-Dzq-MytE.js +1 -0
- package/dist/assets/use-live-actor-state-DErbWQWF.js +1 -0
- package/dist/assets/use-selection-DbR73-xd.js +1 -0
- package/dist/assets/user-data-BB2Of6Ht.js +1 -0
- package/dist/assets/utility-view-state-BHypxA33.js +1 -0
- package/dist/assets/uv-editing.layout-BEKHxmHw.js +1 -0
- package/dist/assets/vgai-shared-fontawesome-CU65D4Ij.js +1 -0
- package/dist/assets/vgai-shared-jsx-dev-runtime-DFDKL7fx.js +1 -0
- package/dist/assets/vgai-shared-jsx-runtime-DIqBpTut.js +1 -0
- package/dist/assets/vgai-shared-react-BT7rMc2H.js +1 -0
- package/dist/assets/vgai-shared-react-dom-DFajMqMT.js +1 -0
- package/dist/assets/vgai-shared-react-dom-client-S0ozfl6_.js +1 -0
- package/dist/assets/vgai-shared-three-Bc5PvpLM.js +1 -0
- package/dist/assets/viewport-pick-context-DkawXbEi.js +1 -0
- package/dist/assets/viewport-root-presentation-CWsE56K6.js +1 -0
- package/dist/assets/viewport-shading-BuA88e_A.js +1 -0
- package/dist/assets/viewport-shading-boundary-B_-_gABg.js +82 -0
- package/dist/assets/viewport-surface-status-B3om4Kcf.js +1 -0
- package/dist/assets/vision_bundle-DX0dWqEa.js +15 -0
- package/dist/assets/voxel-surface-DtcsSWRL.js +1 -0
- package/dist/assets/wav-encode-D31zWF6O.js +1 -0
- package/dist/assets/webgl-frame-capture-FlF9zQmW.js +1 -0
- package/dist/assets/webgl-gpu-timer-D3YX7RaD.js +1 -0
- package/dist/assets/webworkerAll-CM2o_dnz.js +1 -0
- package/dist/assets/worker-UrMcrc5_.js +2 -0
- package/dist/assets/world-documents-147tiFZe.js +1 -0
- package/dist/assets/world-root-stage-binding-vwTrtBz6.js +1 -0
- package/dist/assets/world-state-BWG-IcOd.js +1 -0
- package/dist/assets/writer-B-KovzkA.js +5 -0
- package/dist/assets/xstate-animation-binding-Brd0NST6.js +1 -0
- package/dist/assets/xstate-animation-meta-Q3u9zh_m.js +1 -0
- package/dist/assets/xstate-behavior.action-B8UluFBK.js +1 -0
- package/dist/assets/xstate-behavior.document-CKAsE45I.js +1 -0
- package/dist/assets/xstate-behavior.inspector-vJJyuifc.js +1 -0
- package/dist/assets/xstate-behavior.menu-BXbPICIh.js +1 -0
- package/dist/assets/xstate-inspection-BsZv8LS9.js +1 -0
- package/dist/vgai-shared-react.json +10 -0
- package/dist/vgai-shared-three.json +3 -0
- package/dist-node/cli.js +9320 -0
- package/dist-node/create.js +3239 -0
- package/package.json +91 -0
- package/presets.mjs +2 -0
- package/src/contributions.d.ts +4 -0
- package/src/index.ts +38 -0
- package/starter/cube.blend +0 -0
- package/starter/cube.py +81 -0
- package/template/.agents/references/project-manual.md +341 -0
- package/template/.agents/skills/editor/SKILL.md +53 -0
- package/template/.agents/skills/editor/agents/openai.yaml +4 -0
- package/template/.agents/skills/editor/references/editor-control.md +363 -0
- package/template/.agents/skills/vgai-3d-models/SKILL.md +64 -0
- package/template/.agents/skills/vgai-ingest-existing-game/SKILL.md +112 -0
- package/template/.agents/skills/vgai-threejs-authoring/SKILL.md +347 -0
- package/template/.agents/skills/vgai-threejs-authoring/agents/openai.yaml +4 -0
- package/template/.agents/skills/vgai-threejs-authoring/references/helper-catalog.md +65 -0
- package/template/.claude/skills/editor/SKILL.md +53 -0
- package/template/.claude/skills/editor/agents/openai.yaml +4 -0
- package/template/.claude/skills/editor/references/editor-control.md +363 -0
- package/template/.claude/skills/vgai-3d-models/SKILL.md +64 -0
- package/template/.claude/skills/vgai-ingest-existing-game/SKILL.md +112 -0
- package/template/.claude/skills/vgai-threejs-authoring/SKILL.md +347 -0
- package/template/.claude/skills/vgai-threejs-authoring/agents/openai.yaml +4 -0
- package/template/.claude/skills/vgai-threejs-authoring/references/helper-catalog.md +65 -0
- package/template/.github/copilot-instructions.md +6 -0
- package/template/.github/skills/editor/SKILL.md +53 -0
- package/template/.github/skills/editor/agents/openai.yaml +4 -0
- package/template/.github/skills/editor/references/editor-control.md +363 -0
- package/template/.github/skills/vgai-3d-models/SKILL.md +64 -0
- package/template/.github/skills/vgai-ingest-existing-game/SKILL.md +112 -0
- package/template/.github/skills/vgai-threejs-authoring/SKILL.md +347 -0
- package/template/.github/skills/vgai-threejs-authoring/agents/openai.yaml +4 -0
- package/template/.github/skills/vgai-threejs-authoring/references/helper-catalog.md +65 -0
- package/template/.mcp.json +12 -0
- package/template/.vgai/assets.json +1 -0
- package/template/.vgai/provenance.json +4 -0
- package/template/.volter/tracker/validation/.preset.base.mts +178 -0
- package/template/.volter/tracker/validation/preset.mts +186 -0
- package/template/.volter/tracker/visualizer/.extension.base.tsx +14 -0
- package/template/.volter/tracker/visualizer/extension.tsx +14 -0
- package/template/.volter/tracker-config.json +17 -0
- package/template/AGENTS.md +176 -0
- package/template/CLAUDE.md +1 -0
- package/template/DEVLOG.md +20 -0
- package/template/IDIOMS.md +118 -0
- package/template/LICENSE +30 -0
- package/template/README.md +83 -0
- package/template/ROADMAP.md +20 -0
- package/template/asset-manifest.json +3 -0
- package/template/check-idioms.ts +1533 -0
- package/template/index.html +18 -0
- package/template/manifest-entry-modules-plugin.ts +53 -0
- package/template/package.json +62 -0
- package/template/public/jsm/libs/basis/basis_transcoder.js +19 -0
- package/template/public/jsm/libs/basis/basis_transcoder.wasm +0 -0
- package/template/public/jsm/libs/draco/gltf/draco_decoder.js +33 -0
- package/template/public/jsm/libs/draco/gltf/draco_decoder.wasm +0 -0
- package/template/public/jsm/libs/draco/gltf/draco_encoder.js +33 -0
- package/template/public/jsm/libs/draco/gltf/draco_wasm_wrapper.js +116 -0
- package/template/public/shaders/daylight-sky.frag.glsl +15 -0
- package/template/public/shaders/daylight-sky.vert.glsl +7 -0
- package/template/scripts/patch-drei-stats-gl.mjs +35 -0
- package/template/server/colyseus-setup.ts +111 -0
- package/template/server/loopback.ts +76 -0
- package/template/server/main.ts +78 -0
- package/template/server/rooms/arena-room.ts +488 -0
- package/template/server/rooms/game-room.ts +274 -0
- package/template/server/rooms.ts +15 -0
- package/template/server/tsconfig.json +19 -0
- package/template/src/bot/QaTester.tsx +35 -0
- package/template/src/bot/behaviors.ts +203 -0
- package/template/src/bot/bot-station.ts +192 -0
- package/template/src/bot/tester-run.ts +238 -0
- package/template/src/bot/tester-station.ts +193 -0
- package/template/src/components/environment/DaylightSky.tsx +54 -0
- package/template/src/components/environment/Ground.tsx +129 -0
- package/template/src/components/environment/SceneAtmosphere.tsx +14 -0
- package/template/src/components/environment/SoftGroundShadow.tsx +62 -0
- package/template/src/components/environment/SunShadowFit.tsx +141 -0
- package/template/src/contributions/README.md +98 -0
- package/template/src/contributions/analytics.analytics.tsx +26 -0
- package/template/src/contributions/data.document.tsx +35 -0
- package/template/src/contributions/pages.finder.ts +61 -0
- package/template/src/contributions/tester.inspector.tsx +64 -0
- package/template/src/contributions/use-game-modules.ts +80 -0
- package/template/src/data/README.md +17 -0
- package/template/src/input.ts +97 -0
- package/template/src/lib/audio/capability.json +4 -0
- package/template/src/lib/audio/sfx.ts +255 -0
- package/template/src/lib/react-root/capability.json +4 -0
- package/template/src/lib/react-root/index.ts +20 -0
- package/template/src/lib/react-root/react-root-adapter.tsx +57 -0
- package/template/src/lib/reflections/ReflectionProbe.tsx +201 -0
- package/template/src/lib/reflections/capability.json +4 -0
- package/template/src/lib/reflections/index.ts +19 -0
- package/template/src/lib/reflections/probe-controller.ts +75 -0
- package/template/src/lib/reflections/reflection-probe-registry.ts +402 -0
- package/template/src/lib/reflections/volume-reflection-material.ts +416 -0
- package/template/src/lib/static-batch/Frozen.tsx +91 -0
- package/template/src/lib/static-batch/capability.json +4 -0
- package/template/src/lib/static-batch/freeze.ts +579 -0
- package/template/src/lib/static-batch/index.ts +44 -0
- package/template/src/lib/static-batch/mutation-watch.ts +149 -0
- package/template/src/lib/static-batch/shadow-cache.ts +171 -0
- package/template/src/main.ts +63 -0
- package/template/src/manifest-entry-modules.d.ts +3 -0
- package/template/src/prefabs/HeroBox.stories.tsx +23 -0
- package/template/src/prefabs/HeroBox.tsx +35 -0
- package/template/src/scenes/MainScene.tsx +77 -0
- package/template/src/world.tsx +87 -0
- package/template/tsconfig.json +35 -0
- package/template/tsconfig.node.json +32 -0
- package/template/tsconfig.tools.json +22 -0
- package/template/validate-asset-content.ts +27 -0
- package/template/validate-assets.ts +60 -0
- package/template/validate-manifest.ts +55 -0
- package/template/vgai.adapter.ts +76 -0
- package/template/vgai.project.json +24 -0
- package/template/vite.config.ts +247 -0
- package/template-dotfiles/gitignore +63 -0
- package/workbench/src/media/game-cover.css +83 -0
- package/workbench/src/product.contribution.ts +188 -0
- package/workbench/src/vgaiGameSkew.ts +276 -0
|
@@ -0,0 +1,1533 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { basename, dirname, extname, join, resolve, sep } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Usage: npx tsx check-idioms.ts [folder] [--strict]
|
|
9
|
+
//
|
|
10
|
+
// A dependency-free static drift scanner for the "idiomatic engine usage"
|
|
11
|
+
// checklist shipped alongside this file as IDIOMS.md — read that first, it's
|
|
12
|
+
// the one-page human-readable version of every rule below (⚙ = checked here,
|
|
13
|
+
// 👁 = review-only, not machine-checkable). Shipped IN the project template,
|
|
14
|
+
// mirroring validate-assets.ts/validate-manifest.ts's shape (same
|
|
15
|
+
// project-root-arg convention, same fs-walk-and-report style, zero deps
|
|
16
|
+
// beyond node:fs/node:path — plus node:child_process for the git history
|
|
17
|
+
// queries of W7 and W9, the only rules here asking a question the file tree
|
|
18
|
+
// cannot answer on its own).
|
|
19
|
+
//
|
|
20
|
+
// Two severities:
|
|
21
|
+
// ERROR (exit 1 always) — unambiguous drift, one canonical fix each:
|
|
22
|
+
// E1. `playwright`/`@playwright/test` imported anywhere in the project —
|
|
23
|
+
// the resident tester (directed through its running game module via
|
|
24
|
+
// `vgai eval`) is the ONE bot harness, and a
|
|
25
|
+
// hand-rolled browser boot is banned.
|
|
26
|
+
// VALUE imports only: a type-only import (`import type {Page}`, or
|
|
27
|
+
// an inline `{ type Page }` with no value specifier) binds nothing
|
|
28
|
+
// that could boot a browser, so it never flags.
|
|
29
|
+
// `@volter/game-live` re-exports the Playwright types a `game.page(step)`
|
|
30
|
+
// step normally needs, so the direct import is usually avoidable
|
|
31
|
+
// anyway.
|
|
32
|
+
// E2. package.json scripts invoking `playwright` directly.
|
|
33
|
+
// E3. `window.__vgai` accessed in src/** (the bot/CLI consumer
|
|
34
|
+
// surface, not the authoring surface — export the state from a module).
|
|
35
|
+
// E4. `?bot=`-style query-param bot plumbing in src/** — the project's
|
|
36
|
+
// resident tester owns bot entry.
|
|
37
|
+
// E5. raw `Math.random()`/`Date.now()`/`performance.now()` in
|
|
38
|
+
// src/scripts/** gameplay code — ONLY when this project's OWN
|
|
39
|
+
// vgai.project.json declares `determinism.seededRandom: true`; silent
|
|
40
|
+
// when undeclared (D15, T-D15.3's third enforcer leg — the other
|
|
41
|
+
// two, the engine-src permanent scan and the examples/ burn-down
|
|
42
|
+
// scan, live in the engine repo's own test suite; see
|
|
43
|
+
// packages/engine/test/gameplay-rng-ban.test.ts).
|
|
44
|
+
// E6. `VGAI_STUB_UNIMPLEMENTED` remains in src/tools/** or src/contributions/** — this
|
|
45
|
+
// is the starter's explicit unfinished-work sentinel, so a completion
|
|
46
|
+
// check containing it cannot be green. Replace the Tester/Data/Analytics stub
|
|
47
|
+
// whole in the game's vocabulary; a game with no content tables may
|
|
48
|
+
// delete the Data stub.
|
|
49
|
+
// E9. nonzero R3F authoring warnings (the `R3F00x` codes — unnamed
|
|
50
|
+
// spatial instances, unforwarded transforms, multiple spatial
|
|
51
|
+
// roots). SHIP-TIER (below).
|
|
52
|
+
// E10. a prefab module under `src/prefabs/**` with no colocated
|
|
53
|
+
// `<Name>.stories.tsx`, or whose story does not declare that component.
|
|
54
|
+
// Always an error: the folder already declares prefab intent.
|
|
55
|
+
// E11. anything under `src/scenes/` that is not a complete `*Scene.tsx`
|
|
56
|
+
// composition, or any story there. Folder names state what a file IS.
|
|
57
|
+
// E12. automated-test furniture anywhere in the project — a `*.test.*`/
|
|
58
|
+
// `*.spec.*` file, a test-runner dependency (vitest/jest/mocha/ava),
|
|
59
|
+
// or a script invoking one. Games are fundamentally unpredictable in
|
|
60
|
+
// behavior, so a pre-written assertion file enters an endless cycle
|
|
61
|
+
// of rewriting assertions and actions (owner ruling 2026-08-21).
|
|
62
|
+
// Playtesting is INTERACTIVE — eval + cheats + bot behavior + event
|
|
63
|
+
// logs read on an interval, redirected live until satisfied — and
|
|
64
|
+
// the play log (`logs/play-*.jsonl`) is the playtest's receipt.
|
|
65
|
+
// SHIP-TIER (E9) — the required naming floor for shipped 3D source (owner
|
|
66
|
+
// decision 2026-08-10; `.agents/skills/vgai-3d-assets/SKILL.md`, "The
|
|
67
|
+
// REQUIRED floor"). E9 reports as a WARNING with a standing count the
|
|
68
|
+
// operator reads. An unnamed primitive is normal mid-build;
|
|
69
|
+
// declaring a prefab folder entry without its registration is not.
|
|
70
|
+
// WARN (exit 0 unless --strict) — heuristics, more prone to false
|
|
71
|
+
// positives, escape hatch is the suppression comment below:
|
|
72
|
+
// W1. setInterval/setTimeout in a game-state UI file that owns React state
|
|
73
|
+
// (`src/ui/**` and every manifest-declared dom root entry) — looks like
|
|
74
|
+
// a manual polling pump instead of an ordinary project-store
|
|
75
|
+
// subscription.
|
|
76
|
+
// W3. a static top-level Colyseus import in src/** — pulls the
|
|
77
|
+
// networking client into single-player bundles unconditionally.
|
|
78
|
+
// W4. a display-name equality check (`.name ===` / `.name !==`) inside
|
|
79
|
+
// an onTriggerEnter/onTriggerExit body in src/** — identity belongs
|
|
80
|
+
// to components; an editor rename silently breaks name-gated logic.
|
|
81
|
+
// W8. a POSITIVE numeric `useFrame(cb, N)` priority in src/** on a
|
|
82
|
+
// callback that does not itself render. In react-three-fiber a
|
|
83
|
+
// positive priority means "I am taking over rendering": fiber stops
|
|
84
|
+
// calling `gl.render` and draws nothing unless a callback does. The
|
|
85
|
+
// world goes BLANK while state, mounts, logs and health checks all
|
|
86
|
+
// stay green — the one failure this scanner exists to catch and
|
|
87
|
+
// previously reported as 0 findings. Silent for 0 (the default) and
|
|
88
|
+
// for negatives (which only ORDER callbacks), and silent when the
|
|
89
|
+
// callback calls `.render(` itself, which is what taking over
|
|
90
|
+
// actually looks like.
|
|
91
|
+
// W9. gameplay source has accumulated since this project was scaffolded
|
|
92
|
+
// with NO evidence Play has ever run — no `logs/play-*.jsonl`
|
|
93
|
+
// receipt. "Author in playable slices" is the
|
|
94
|
+
// doctrine this mechanizes: a whole game written before the first
|
|
95
|
+
// Play is how pixel-only bugs (blank canvas, invisible mesh, camera
|
|
96
|
+
// inside geometry) survive every green typecheck and test. Same
|
|
97
|
+
// git-history discipline as W7, and SILENT whenever the question
|
|
98
|
+
// cannot be asked (no git, no scaffold anchor, no gameplay commits
|
|
99
|
+
// yet) or has already been answered (any play evidence at all).
|
|
100
|
+
// W10. the manifest declares a Three root but the project contains no
|
|
101
|
+
// correctly story-backed world prefab. Fully procedural or wholly
|
|
102
|
+
// simulation-owned worlds may be honest exceptions; ordinary games
|
|
103
|
+
// should not silently lose the prefab architecture when replacing
|
|
104
|
+
// the starter subject.
|
|
105
|
+
//
|
|
106
|
+
// E9 is the one rule that does NOT answer its question from the file tree
|
|
107
|
+
// alone: the R3F00x codes come from the editor's own source analyzer
|
|
108
|
+
// (`ui-source/r3f-project-contracts.ts`), the same pass validate-on-save and
|
|
109
|
+
// `vgai status` report from. It is loaded OFFLINE — the analyzer is a pure
|
|
110
|
+
// `(code, file) => diagnostics` function over `typescript`, with no dev
|
|
111
|
+
// server, browser or editor session anywhere in it — through this project's
|
|
112
|
+
// own `@editor/*` tsconfig mapping, so check-idioms never requires a running
|
|
113
|
+
// editor. See `loadR3fAnalyzer`.
|
|
114
|
+
//
|
|
115
|
+
// Suppression: put `// idioms-ignore <rule-id> <reason>` on the flagged
|
|
116
|
+
// line. The reason is REQUIRED — an empty/missing reason does not suppress
|
|
117
|
+
// (the finding is still reported, with a note that the comment is
|
|
118
|
+
// incomplete). This is intentionally a dumb line/regex scanner, not an AST
|
|
119
|
+
// pass — it comment-strips PURE comment lines (lines whose trimmed text
|
|
120
|
+
// starts with `//`, `/*`, `*`, or is exactly `*/`) before matching rule
|
|
121
|
+
// patterns, so doc-comment prose that merely MENTIONS e.g. `window.__vgai`
|
|
122
|
+
// doesn't trip a finding, but it does not understand string literals,
|
|
123
|
+
// template strings, or multi-statement lines. False positives are expected
|
|
124
|
+
// occasionally; suppress them with a reason rather than fighting the regex.
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
type Severity = 'error' | 'warn';
|
|
128
|
+
|
|
129
|
+
interface Finding {
|
|
130
|
+
file: string; // relative to rootDir, forward-slash separated
|
|
131
|
+
line: number;
|
|
132
|
+
severity: Severity;
|
|
133
|
+
rule: string; // e.g. "E1"
|
|
134
|
+
idiom: string; // short name — matches an IDIOMS.md item
|
|
135
|
+
message: string;
|
|
136
|
+
fix: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const args = process.argv.slice(2);
|
|
140
|
+
const strict = args.includes('--strict');
|
|
141
|
+
const positional = args.filter((a) => a !== '--strict');
|
|
142
|
+
const rootDir = resolve(positional[0] ?? '.');
|
|
143
|
+
|
|
144
|
+
if (!existsSync(rootDir)) {
|
|
145
|
+
console.error(`No such directory: ${rootDir}`);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// This script's own resolved path — excluded from every scan so its rule
|
|
150
|
+
// regexes (which literally contain the strings they look for, e.g.
|
|
151
|
+
// 'colyseus.js', 'window.__vgai', '@playwright/test') never self-flag.
|
|
152
|
+
const SELF_PATH = resolve(fileURLToPath(import.meta.url));
|
|
153
|
+
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// File collection
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
// Agent instructions and their vendor projections may legitimately contain
|
|
159
|
+
// executable reference helpers (including Playwright-based capture scripts).
|
|
160
|
+
// They are tooling context, never game source, and must not poison a generated
|
|
161
|
+
// project's idiom scan.
|
|
162
|
+
const SKIP_DIRS = new Set([
|
|
163
|
+
'node_modules',
|
|
164
|
+
'.git',
|
|
165
|
+
'.agents',
|
|
166
|
+
'.claude',
|
|
167
|
+
'.github',
|
|
168
|
+
'dist',
|
|
169
|
+
'build',
|
|
170
|
+
'.vgai',
|
|
171
|
+
'coverage',
|
|
172
|
+
'.turbo',
|
|
173
|
+
]);
|
|
174
|
+
const CODE_EXT = new Set(['.ts', '.tsx', '.js', '.jsx']);
|
|
175
|
+
|
|
176
|
+
function walk(dir: string, out: string[]): void {
|
|
177
|
+
if (!existsSync(dir)) return;
|
|
178
|
+
for (const entry of readdirSync(dir)) {
|
|
179
|
+
const full = join(dir, entry);
|
|
180
|
+
if (SKIP_DIRS.has(entry)) continue;
|
|
181
|
+
if (statSync(full).isDirectory()) {
|
|
182
|
+
walk(full, out);
|
|
183
|
+
} else {
|
|
184
|
+
out.push(full);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function relPath(file: string): string {
|
|
190
|
+
return resolve(file)
|
|
191
|
+
.slice(rootDir.length + 1)
|
|
192
|
+
.split(sep)
|
|
193
|
+
.join('/');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const allFiles: string[] = [];
|
|
197
|
+
walk(rootDir, allFiles);
|
|
198
|
+
|
|
199
|
+
const codeFiles = allFiles
|
|
200
|
+
.filter((f) => CODE_EXT.has(extname(f)))
|
|
201
|
+
.filter((f) => resolve(f) !== SELF_PATH);
|
|
202
|
+
|
|
203
|
+
const srcFiles = codeFiles.filter((f) => relPath(f).startsWith('src/'));
|
|
204
|
+
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
// Comment-stripping + suppression helpers
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
interface FileText {
|
|
210
|
+
rawLines: string[];
|
|
211
|
+
/** Same line count as rawLines; pure-comment lines are blanked so rule
|
|
212
|
+
* regexes never match prose inside a doc comment. Joined with '\n' when a
|
|
213
|
+
* rule needs to match across a multi-line statement (e.g. a wrapped
|
|
214
|
+
* import). */
|
|
215
|
+
blanked: string;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function readFileText(file: string): FileText {
|
|
219
|
+
const raw = readFileSync(file, 'utf-8');
|
|
220
|
+
const rawLines = raw.split('\n');
|
|
221
|
+
const blankedLines = rawLines.map((line) => {
|
|
222
|
+
const t = line.trim();
|
|
223
|
+
if (t.startsWith('//') || t.startsWith('/*') || t.startsWith('*') || t === '*/') return '';
|
|
224
|
+
return line;
|
|
225
|
+
});
|
|
226
|
+
return { rawLines, blanked: blankedLines.join('\n') };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function lineNumberAt(text: string, index: number): number {
|
|
230
|
+
let n = 1;
|
|
231
|
+
for (let i = 0; i < index && i < text.length; i++) {
|
|
232
|
+
if (text[i] === '\n') n++;
|
|
233
|
+
}
|
|
234
|
+
return n;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Looks for `idioms-ignore <rule> <reason>` on the given raw source line. */
|
|
238
|
+
function suppressionStatus(
|
|
239
|
+
rawLine: string,
|
|
240
|
+
ruleId: string,
|
|
241
|
+
): 'suppressed' | 'missing-reason' | 'none' {
|
|
242
|
+
const m = rawLine.match(/idioms-ignore\s+(\S+)(?:\s+(.*))?$/);
|
|
243
|
+
if (!m) return 'none';
|
|
244
|
+
const [, rule, reasonRaw] = m;
|
|
245
|
+
if (rule !== ruleId) return 'none';
|
|
246
|
+
const reason = (reasonRaw ?? '').trim();
|
|
247
|
+
return reason.length > 0 ? 'suppressed' : 'missing-reason';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const findings: Finding[] = [];
|
|
251
|
+
|
|
252
|
+
function addFinding(
|
|
253
|
+
file: string,
|
|
254
|
+
rawLines: string[],
|
|
255
|
+
lineNo: number,
|
|
256
|
+
rule: string,
|
|
257
|
+
severity: Severity,
|
|
258
|
+
idiom: string,
|
|
259
|
+
message: string,
|
|
260
|
+
fix: string,
|
|
261
|
+
): void {
|
|
262
|
+
const rawLine = rawLines[lineNo - 1] ?? '';
|
|
263
|
+
const status = suppressionStatus(rawLine, rule);
|
|
264
|
+
if (status === 'suppressed') return;
|
|
265
|
+
const finalMessage =
|
|
266
|
+
status === 'missing-reason'
|
|
267
|
+
? `${message} [an "idioms-ignore ${rule}" comment is present but has no reason — NOT suppressed]`
|
|
268
|
+
: message;
|
|
269
|
+
findings.push({
|
|
270
|
+
file: relPath(file),
|
|
271
|
+
line: lineNo,
|
|
272
|
+
severity,
|
|
273
|
+
rule,
|
|
274
|
+
idiom,
|
|
275
|
+
message: finalMessage,
|
|
276
|
+
fix,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Project-wide (no single line) findings, e.g. "zero registrations anywhere
|
|
281
|
+
* in src/". Suppressed by an `idioms-ignore <rule> <reason>` comment
|
|
282
|
+
* ANYWHERE inside src/**. */
|
|
283
|
+
function addProjectFinding(
|
|
284
|
+
rule: string,
|
|
285
|
+
severity: Severity,
|
|
286
|
+
idiom: string,
|
|
287
|
+
message: string,
|
|
288
|
+
fix: string,
|
|
289
|
+
): void {
|
|
290
|
+
for (const file of srcFiles) {
|
|
291
|
+
const { rawLines } = readFileText(file);
|
|
292
|
+
for (const rawLine of rawLines) {
|
|
293
|
+
if (suppressionStatus(rawLine, rule) === 'suppressed') return;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
findings.push({ file: 'src/', line: 0, severity, rule, idiom, message, fix });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
// E1 — playwright imported anywhere in the project
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
/** True when an import clause — everything between the `import` keyword and
|
|
304
|
+
* `from` — binds NO value at all.
|
|
305
|
+
*
|
|
306
|
+
* E1's rationale is about value bindings: the objection is a hand-rolled
|
|
307
|
+
* browser boot. A type-only import binds nothing usable — under
|
|
308
|
+
* `verbatimModuleSyntax` the inline form degenerates to a bare
|
|
309
|
+
* `import {} from '…'` module load, which still cannot
|
|
310
|
+
* launch a browser — so E1 must not fire on it. (`type` is not a reserved
|
|
311
|
+
* word, hence the care below: `import type from 'x'` is a DEFAULT import of
|
|
312
|
+
* a binding literally named `type`, and `{ type as T }` renames that same
|
|
313
|
+
* binding — both are real value imports.)
|
|
314
|
+
*/
|
|
315
|
+
function bindsNoValue(clause: string): boolean {
|
|
316
|
+
const c = clause.trim();
|
|
317
|
+
// Whole-clause form: `import type {A}` / `import type * as ns` /
|
|
318
|
+
// `import type Default`. Requires a real clause after the keyword, which
|
|
319
|
+
// is what separates it from `import type from '…'` (clause is just `type`)
|
|
320
|
+
// and `import type, {A} from '…'` (a default binding named `type`).
|
|
321
|
+
if (/^type\s*[{*]/.test(c) || /^type\s+[A-Za-z_$]/.test(c)) return true;
|
|
322
|
+
// Inline form: type-only iff EVERY named specifier carries `type`. A mixed
|
|
323
|
+
// `{ test, type Page }` keeps its value binding and still flags.
|
|
324
|
+
const named = c.match(/^\{([^}]*)\}$/);
|
|
325
|
+
if (!named) return false;
|
|
326
|
+
const specifiers = named[1]
|
|
327
|
+
.split(',')
|
|
328
|
+
.map((s) => s.trim())
|
|
329
|
+
.filter((s) => s.length > 0);
|
|
330
|
+
return specifiers.length > 0 && specifiers.every((s) => /^type\s+(?!as\s)/.test(s));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function checkPlaywrightImports(): void {
|
|
334
|
+
const importRe =
|
|
335
|
+
/\bimport\b([^;]*?)\bfrom\s+['"](@playwright\/test|playwright)['"]|\brequire\(\s*['"](@playwright\/test|playwright)['"]\s*\)/g;
|
|
336
|
+
for (const file of codeFiles) {
|
|
337
|
+
const { rawLines, blanked } = readFileText(file);
|
|
338
|
+
for (const m of blanked.matchAll(importRe)) {
|
|
339
|
+
const pkg = m[2] ?? m[3];
|
|
340
|
+
// `require(...)` (the m[3] branch) has no clause and is always a value.
|
|
341
|
+
if (m[1] !== undefined && bindsNoValue(m[1])) continue;
|
|
342
|
+
const lineNo = lineNumberAt(blanked, m.index);
|
|
343
|
+
addFinding(
|
|
344
|
+
file,
|
|
345
|
+
rawLines,
|
|
346
|
+
lineNo,
|
|
347
|
+
'E1',
|
|
348
|
+
'error',
|
|
349
|
+
'bot-canonical',
|
|
350
|
+
`"${pkg}" imported — the resident tester, directed through the running game's own module, is the ONE bot harness; hand-rolled Playwright boots are banned.`,
|
|
351
|
+
"Drive the game through the live session (vgai eval + the running tester module), or take the Playwright types from '@volter/game-live' as a type-only import.",
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
// E2 — package.json scripts invoking playwright directly
|
|
359
|
+
// ---------------------------------------------------------------------------
|
|
360
|
+
|
|
361
|
+
function checkPackageJsonScripts(): void {
|
|
362
|
+
const pkgPath = join(rootDir, 'package.json');
|
|
363
|
+
if (!existsSync(pkgPath)) return;
|
|
364
|
+
const raw = readFileSync(pkgPath, 'utf-8');
|
|
365
|
+
const rawLines = raw.split('\n');
|
|
366
|
+
let pkg: { scripts?: Record<string, unknown> };
|
|
367
|
+
try {
|
|
368
|
+
pkg = JSON.parse(raw);
|
|
369
|
+
} catch {
|
|
370
|
+
return; // malformed JSON is not this scanner's job to report
|
|
371
|
+
}
|
|
372
|
+
for (const [name, cmd] of Object.entries(pkg.scripts ?? {})) {
|
|
373
|
+
if (typeof cmd !== 'string' || !/\bplaywright\b/.test(cmd)) continue;
|
|
374
|
+
const lineIdx = rawLines.findIndex((l) => l.includes(`"${name}"`) && l.includes(':'));
|
|
375
|
+
const lineNo = lineIdx >= 0 ? lineIdx + 1 : 1;
|
|
376
|
+
addFinding(
|
|
377
|
+
pkgPath,
|
|
378
|
+
rawLines,
|
|
379
|
+
lineNo,
|
|
380
|
+
'E2',
|
|
381
|
+
'error',
|
|
382
|
+
'bot-canonical',
|
|
383
|
+
`script "${name}" invokes playwright directly ("${cmd}") — this project has no Playwright lane to own a browser binary or config.`,
|
|
384
|
+
`Delete the "${name}" script — playtesting is the live session (vgai eval + the running tester module), not a script.`,
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ---------------------------------------------------------------------------
|
|
390
|
+
// E12 — automated-test furniture in a game project
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
|
|
393
|
+
/** Games are fundamentally unpredictable in behavior: a pre-written assertion
|
|
394
|
+
* file enters an endless cycle of rewriting assertions and actions (owner
|
|
395
|
+
* ruling, 2026-08-21 — measured live: three assertion renegotiations in
|
|
396
|
+
* twenty minutes on one balance suite). The playtest loop is INTERACTIVE:
|
|
397
|
+
* `vgai eval` + cheats set the situation, bot policy runs behavior, event
|
|
398
|
+
* logs are read on an interval, and the run is redirected live until the
|
|
399
|
+
* operator is satisfied. The play log (logs/play-*.jsonl) is the receipt. This
|
|
400
|
+
* rule keeps the furniture out so the cheapest path stays the sanctioned
|
|
401
|
+
* one. */
|
|
402
|
+
function checkNoTestFurniture(): void {
|
|
403
|
+
const E12_FIX =
|
|
404
|
+
'Delete it and playtest LIVE: eval + cheats set the situation, bot goals run behavior, the ' +
|
|
405
|
+
'event log is read on an interval, the run is redirected until satisfied; the play log ' +
|
|
406
|
+
'(logs/play-*.jsonl) is the receipt.';
|
|
407
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', '.vgai', 'logs', 'media', 'public']);
|
|
408
|
+
const testFile = /\.(test|spec)\.[cm]?[jt]sx?$/;
|
|
409
|
+
const walk = (dir: string): void => {
|
|
410
|
+
for (const entry of readdirSync(dir)) {
|
|
411
|
+
if (SKIP_DIRS.has(entry)) continue;
|
|
412
|
+
const full = join(dir, entry);
|
|
413
|
+
if (statSync(full).isDirectory()) {
|
|
414
|
+
walk(full);
|
|
415
|
+
} else if (testFile.test(entry)) {
|
|
416
|
+
const { rawLines } = readFileText(full);
|
|
417
|
+
addFinding(
|
|
418
|
+
full,
|
|
419
|
+
rawLines,
|
|
420
|
+
1,
|
|
421
|
+
'E12',
|
|
422
|
+
'error',
|
|
423
|
+
'no-automated-tests',
|
|
424
|
+
'an automated-test file in a game project — games are fundamentally unpredictable in ' +
|
|
425
|
+
'behavior, so a pre-written assertion file enters an endless cycle of rewriting ' +
|
|
426
|
+
'assertions and actions. Playtesting is interactive (see AGENTS.md).',
|
|
427
|
+
E12_FIX,
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
walk(rootDir);
|
|
433
|
+
|
|
434
|
+
const pkgPath = join(rootDir, 'package.json');
|
|
435
|
+
if (!existsSync(pkgPath)) return;
|
|
436
|
+
const raw = readFileSync(pkgPath, 'utf-8');
|
|
437
|
+
const rawLines = raw.split('\n');
|
|
438
|
+
let pkg: {
|
|
439
|
+
scripts?: Record<string, unknown>;
|
|
440
|
+
dependencies?: Record<string, unknown>;
|
|
441
|
+
devDependencies?: Record<string, unknown>;
|
|
442
|
+
};
|
|
443
|
+
try {
|
|
444
|
+
pkg = JSON.parse(raw);
|
|
445
|
+
} catch {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const runners = ['vitest', 'jest', 'mocha', 'ava', 'uvu', '@playwright/test'];
|
|
449
|
+
for (const dep of runners) {
|
|
450
|
+
if (pkg.dependencies?.[dep] === undefined && pkg.devDependencies?.[dep] === undefined) continue;
|
|
451
|
+
const lineIdx = rawLines.findIndex((l) => l.includes(`"${dep}"`));
|
|
452
|
+
addFinding(
|
|
453
|
+
pkgPath,
|
|
454
|
+
rawLines,
|
|
455
|
+
lineIdx >= 0 ? lineIdx + 1 : 1,
|
|
456
|
+
'E12',
|
|
457
|
+
'error',
|
|
458
|
+
'no-automated-tests',
|
|
459
|
+
`test runner "${dep}" declared as a dependency — a game project ships no automated tests; ` +
|
|
460
|
+
'the playtest is interactive and the play log is its receipt.',
|
|
461
|
+
E12_FIX,
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
for (const [name, cmd] of Object.entries(pkg.scripts ?? {})) {
|
|
465
|
+
if (typeof cmd !== 'string' || !/\b(vitest|jest|mocha|ava|uvu)\b/.test(cmd)) continue;
|
|
466
|
+
const lineIdx = rawLines.findIndex((l) => l.includes(`"${name}"`) && l.includes(':'));
|
|
467
|
+
addFinding(
|
|
468
|
+
pkgPath,
|
|
469
|
+
rawLines,
|
|
470
|
+
lineIdx >= 0 ? lineIdx + 1 : 1,
|
|
471
|
+
'E12',
|
|
472
|
+
'error',
|
|
473
|
+
'no-automated-tests',
|
|
474
|
+
`script "${name}" invokes a test runner ("${cmd}") — a game project ships no automated tests.`,
|
|
475
|
+
E12_FIX,
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// ---------------------------------------------------------------------------
|
|
481
|
+
// E3 — window.__vgai accessed in src/**
|
|
482
|
+
// ---------------------------------------------------------------------------
|
|
483
|
+
|
|
484
|
+
function checkWindowVgai(): void {
|
|
485
|
+
// Two-part, same-line heuristic rather than a single `window\.__vgai`
|
|
486
|
+
// regex: this codebase's OWN canonical way to reach the bridge (see
|
|
487
|
+
// @volter/game-live's game-client/client.ts) is a cast in between —
|
|
488
|
+
// `(window as unknown as { __vgai?: ... }).__vgai` — so `window` and
|
|
489
|
+
// `.__vgai` are literal-adjacent only coincidentally. Flag any line that
|
|
490
|
+
// mentions the `window` token AND a bare `.__vgai`/`['__vgai']` property
|
|
491
|
+
// access. The negative lookahead on the dotted form keeps this off
|
|
492
|
+
// `__vgaiScene`/`__vgaiCamera`/`__vgaiRender`/`__vgaiBatch` etc. — those
|
|
493
|
+
// are different, unrelated dev-only globals, not the frozen `window.__vgai`
|
|
494
|
+
// debug-bridge shape (D18). A file-scoped `window` ALIAS on its own line
|
|
495
|
+
// (`const w = window as …; w['__vgai']`) is a known miss — this is a
|
|
496
|
+
// line-scan, not an AST pass.
|
|
497
|
+
const windowRe = /\bwindow\b/;
|
|
498
|
+
const vgaiAccessRe = /\.__vgai(?![A-Za-z0-9_])|\[\s*['"]__vgai['"]\s*\]/g;
|
|
499
|
+
for (const file of srcFiles) {
|
|
500
|
+
const { rawLines, blanked } = readFileText(file);
|
|
501
|
+
const blankedLines = blanked.split('\n');
|
|
502
|
+
blankedLines.forEach((line, i) => {
|
|
503
|
+
if (!windowRe.test(line)) return;
|
|
504
|
+
vgaiAccessRe.lastIndex = 0;
|
|
505
|
+
if (!vgaiAccessRe.test(line)) return;
|
|
506
|
+
addFinding(
|
|
507
|
+
file,
|
|
508
|
+
rawLines,
|
|
509
|
+
i + 1,
|
|
510
|
+
'E3',
|
|
511
|
+
'error',
|
|
512
|
+
'bot-canonical',
|
|
513
|
+
'window.__vgai accessed directly in src/** — this is the bot/CLI consumer surface, not the authoring surface.',
|
|
514
|
+
'Read and drive the game through its own exported modules instead (game.run(({ modules }) => …)).',
|
|
515
|
+
);
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// ---------------------------------------------------------------------------
|
|
521
|
+
// E4 — ?bot= query-param bot plumbing in src/**
|
|
522
|
+
// ---------------------------------------------------------------------------
|
|
523
|
+
|
|
524
|
+
function checkBotParam(): void {
|
|
525
|
+
const re = /searchParams\.get\(\s*['"]bot['"]|['"]bot=['"]/g;
|
|
526
|
+
for (const file of srcFiles) {
|
|
527
|
+
const { rawLines, blanked } = readFileText(file);
|
|
528
|
+
for (const m of blanked.matchAll(re)) {
|
|
529
|
+
const lineNo = lineNumberAt(blanked, m.index);
|
|
530
|
+
addFinding(
|
|
531
|
+
file,
|
|
532
|
+
rawLines,
|
|
533
|
+
lineNo,
|
|
534
|
+
'E4',
|
|
535
|
+
'error',
|
|
536
|
+
'bot-canonical',
|
|
537
|
+
'?bot= query-param plumbing found in src/** — the resident tester is game code reached through its running module; hand-rolled boot flags are banned.',
|
|
538
|
+
"Delete this bot-param handling; direct the resident tester through the game's exported tester functions from the live session.",
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// ---------------------------------------------------------------------------
|
|
545
|
+
// W1 — polling pump in a game-state UI
|
|
546
|
+
// ---------------------------------------------------------------------------
|
|
547
|
+
|
|
548
|
+
/** Every file the manifest names as a `dom` root's entry, project-relative and
|
|
549
|
+
* forward-slashed. A dom root IS the React UI over game state, so its entry is
|
|
550
|
+
* in W1's scope whatever it happens to import. Missing/malformed manifest
|
|
551
|
+
* reads as "none" — validating its shape is validate-manifest.ts's job. */
|
|
552
|
+
function manifestDomRootEntries(): Set<string> {
|
|
553
|
+
const entries = new Set<string>();
|
|
554
|
+
const manifestPath = join(rootDir, 'vgai.project.json');
|
|
555
|
+
if (!existsSync(manifestPath)) return entries;
|
|
556
|
+
try {
|
|
557
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8')) as {
|
|
558
|
+
roots?: Array<{ adapter?: unknown; entry?: unknown }>;
|
|
559
|
+
};
|
|
560
|
+
for (const root of manifest.roots ?? []) {
|
|
561
|
+
if (root.adapter === 'dom' && typeof root.entry === 'string') {
|
|
562
|
+
entries.add(root.entry.replace(/^\.\//, ''));
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
} catch {
|
|
566
|
+
/* malformed manifest: no entries, no findings */
|
|
567
|
+
}
|
|
568
|
+
return entries;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Scope is `src/ui/**` plus every manifest-declared dom-root entry. The rule
|
|
573
|
+
* does not look for a particular engine hook: UI state is the project's own
|
|
574
|
+
* React context/store, and prescribing a hook would merely make the scanner
|
|
575
|
+
* blind when the project chose another native store.
|
|
576
|
+
*/
|
|
577
|
+
function pollingPumpScope(file: string, blanked: string, domRootEntries: Set<string>): boolean {
|
|
578
|
+
const rel = relPath(file);
|
|
579
|
+
if (!(rel.startsWith('src/ui/') || domRootEntries.has(rel))) return false;
|
|
580
|
+
// "driving component state" is the rule's actual subject, so the UI leg asks
|
|
581
|
+
// for it: a timer in a UI file that owns no React state is a transition, a
|
|
582
|
+
// debounce, or a one-shot — not a pump.
|
|
583
|
+
return /\b(useState|useReducer|useSyncExternalStore)\s*[(<]/.test(blanked);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function checkPollingPump(): void {
|
|
587
|
+
const timerRe = /\b(setInterval|setTimeout)\s*\(/g;
|
|
588
|
+
const domRootEntries = manifestDomRootEntries();
|
|
589
|
+
for (const file of srcFiles) {
|
|
590
|
+
const { rawLines, blanked } = readFileText(file);
|
|
591
|
+
if (!pollingPumpScope(file, blanked, domRootEntries)) continue;
|
|
592
|
+
for (const m of blanked.matchAll(timerRe)) {
|
|
593
|
+
const lineNo = lineNumberAt(blanked, m.index);
|
|
594
|
+
addFinding(
|
|
595
|
+
file,
|
|
596
|
+
rawLines,
|
|
597
|
+
lineNo,
|
|
598
|
+
'W1',
|
|
599
|
+
'warn',
|
|
600
|
+
'state-via-project-store',
|
|
601
|
+
`${m[1]} driving component state in a game-state UI file — looks like a manual polling pump.`,
|
|
602
|
+
"Subscribe to this game's own React context/store (for example useSyncExternalStore) instead of polling.",
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// ---------------------------------------------------------------------------
|
|
609
|
+
|
|
610
|
+
// ---------------------------------------------------------------------------
|
|
611
|
+
// W3 — static top-level Colyseus import in src/**
|
|
612
|
+
// ---------------------------------------------------------------------------
|
|
613
|
+
|
|
614
|
+
function checkColyseusStaticImport(): void {
|
|
615
|
+
// Matches a static `import ... from '<pkg>'` at the START of a (blanked,
|
|
616
|
+
// trimmed) line only — a dynamic `await import('@colyseus/sdk')` call is
|
|
617
|
+
// NOT a line starting with the `import` keyword, so it's unaffected.
|
|
618
|
+
// Covers both the current SDK package name (`@colyseus/sdk`) and the
|
|
619
|
+
// legacy pre-0.15 package name (`colyseus.js`) doctrine still refers to.
|
|
620
|
+
// Excludes `import type { ... } from '@colyseus/sdk'` — erased entirely at
|
|
621
|
+
// build time (zero runtime bundle impact), so it can't be what this rule's
|
|
622
|
+
// "pulls the client into single-player bundles" rationale is about; the
|
|
623
|
+
// canonical `--example third-person-arena` uses exactly this form for its
|
|
624
|
+
// ambient `Room` type. A mixed `import { Client, type Room } from ...`
|
|
625
|
+
// still flags correctly, since only the leading `import type` form is
|
|
626
|
+
// fully erased.
|
|
627
|
+
const re = /^\s*import\s+(?!type\b).*\bfrom\s+['"](colyseus\.js|@colyseus\/sdk)['"]/;
|
|
628
|
+
for (const file of srcFiles) {
|
|
629
|
+
const { rawLines, blanked } = readFileText(file);
|
|
630
|
+
const blankedLines = blanked.split('\n');
|
|
631
|
+
blankedLines.forEach((line, i) => {
|
|
632
|
+
const m = line.match(re);
|
|
633
|
+
if (!m) return;
|
|
634
|
+
addFinding(
|
|
635
|
+
file,
|
|
636
|
+
rawLines,
|
|
637
|
+
i + 1,
|
|
638
|
+
'W3',
|
|
639
|
+
'warn',
|
|
640
|
+
'networking-colyseus-direct',
|
|
641
|
+
`static top-level "${m[1]}" import in src/** — pulls the networking client into single-player bundles unconditionally.`,
|
|
642
|
+
"Dynamic-import it instead: const { Client } = await import('@colyseus/sdk');",
|
|
643
|
+
);
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// ---------------------------------------------------------------------------
|
|
649
|
+
// W4 — display-name equality checks inside trigger callbacks
|
|
650
|
+
// ---------------------------------------------------------------------------
|
|
651
|
+
|
|
652
|
+
function checkTriggerNameIdentity(): void {
|
|
653
|
+
// Entity identity belongs to explicit project-owned data/components, not to
|
|
654
|
+
// the display name: `other.name === 'Coin'` works
|
|
655
|
+
// until someone renames the entity in the editor's hierarchy panel, then
|
|
656
|
+
// silently never matches again. Scope: only inside an
|
|
657
|
+
// onTriggerEnter/onTriggerExit body — a name comparison elsewhere (e.g.
|
|
658
|
+
// asset lookup by GLTF node name) is a different, legitimate pattern.
|
|
659
|
+
// Body detection is brace-counting from the callback's opening line — the
|
|
660
|
+
// same dumb-line-scan honesty budget as every other rule here.
|
|
661
|
+
const startRe = /\bonTrigger(?:Enter|Exit)\s*[(=]/;
|
|
662
|
+
const nameEqRe = /\.name\s*[!=]==?/;
|
|
663
|
+
for (const file of srcFiles) {
|
|
664
|
+
const { rawLines, blanked } = readFileText(file);
|
|
665
|
+
const lines = blanked.split('\n');
|
|
666
|
+
let i = 0;
|
|
667
|
+
while (i < lines.length) {
|
|
668
|
+
if (!startRe.test(lines[i] ?? '')) {
|
|
669
|
+
i++;
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
let depth = 0;
|
|
673
|
+
let seenOpen = false;
|
|
674
|
+
let j = i;
|
|
675
|
+
for (; j < lines.length; j++) {
|
|
676
|
+
const line = lines[j] ?? '';
|
|
677
|
+
for (const ch of line) {
|
|
678
|
+
if (ch === '{') {
|
|
679
|
+
depth++;
|
|
680
|
+
seenOpen = true;
|
|
681
|
+
} else if (ch === '}') {
|
|
682
|
+
depth--;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
// A signature with no `{` within a few lines is a DECLARATION (interface/
|
|
686
|
+
// abstract member), not a definition — without this bail the scan would
|
|
687
|
+
// run to EOF and flag every `.name ===` in the rest of the file.
|
|
688
|
+
if (!seenOpen) {
|
|
689
|
+
if (j >= i + 3) break;
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
if (nameEqRe.test(line)) {
|
|
693
|
+
addFinding(
|
|
694
|
+
file,
|
|
695
|
+
rawLines,
|
|
696
|
+
j + 1,
|
|
697
|
+
'W4',
|
|
698
|
+
'warn',
|
|
699
|
+
'trigger-name-identity',
|
|
700
|
+
'display-name equality check inside an onTriggerEnter/onTriggerExit body — an editor rename silently breaks name-gated logic.',
|
|
701
|
+
'Attach explicit project-owned identity data to the entities you care about and gate on that, not object.name.',
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
if (seenOpen && depth <= 0) break;
|
|
705
|
+
}
|
|
706
|
+
i = j + 1;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// ---------------------------------------------------------------------------
|
|
712
|
+
// git-history plumbing shared by slice-proxy rules (W9)
|
|
713
|
+
// ---------------------------------------------------------------------------
|
|
714
|
+
// The vgai CLI mirrors this exact measurement (source path, gameplay
|
|
715
|
+
// pathspecs, threshold, silent-on-unaskable) as `vgai status`'s ambient
|
|
716
|
+
// bot-drift line — engine repo, packages/vgai-cli/src/instrumentation-signal.ts.
|
|
717
|
+
// If you change any of the three constants below, change them there too, or
|
|
718
|
+
// the ambient signal and this rule will disagree about whether a bot drifted.
|
|
719
|
+
|
|
720
|
+
/** The files that ARE the game, for slice-proxy measurements (W9). Deliberately
|
|
721
|
+
* NOT all of src/ — churn in src/ui/, src/tools/ or src/data/ is not by
|
|
722
|
+
* itself a gameplay slice. */
|
|
723
|
+
const GAMEPLAY_PATHS = [
|
|
724
|
+
'src/components',
|
|
725
|
+
'src/scenes',
|
|
726
|
+
'src/prefabs',
|
|
727
|
+
'src/lib',
|
|
728
|
+
'src/world.tsx',
|
|
729
|
+
'src/hooks',
|
|
730
|
+
];
|
|
731
|
+
|
|
732
|
+
/** Runs git in the project root. Returns null for ANY failure — git missing,
|
|
733
|
+
* not a repository, unborn HEAD, non-zero exit — because every one of those
|
|
734
|
+
* means "this project cannot be asked the question", not "this project is
|
|
735
|
+
* drifting". */
|
|
736
|
+
function git(args: string[]): string | null {
|
|
737
|
+
const result = spawnSync('git', args, { cwd: rootDir, encoding: 'utf-8' });
|
|
738
|
+
if (result.error || result.status !== 0) return null;
|
|
739
|
+
return (result.stdout ?? '').trim();
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/** Memoized: every git-history rule (W9) opens with this question, and a
|
|
743
|
+
* process spawn is by far the most expensive thing this scanner does. The
|
|
744
|
+
* whole run has to stay cheap enough to invoke constantly — that is the
|
|
745
|
+
* point of a rule like W9, which only pays off when it is run early and
|
|
746
|
+
* often. */
|
|
747
|
+
let insideWorkTree: boolean | null = null;
|
|
748
|
+
function isGitWorkTree(): boolean {
|
|
749
|
+
if (insideWorkTree === null)
|
|
750
|
+
insideWorkTree = git(['rev-parse', '--is-inside-work-tree']) === 'true';
|
|
751
|
+
return insideWorkTree;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// ---------------------------------------------------------------------------
|
|
755
|
+
// W8 — a positive useFrame priority silently takes over rendering
|
|
756
|
+
// ---------------------------------------------------------------------------
|
|
757
|
+
|
|
758
|
+
/** The `(` … `)` span of the call whose open paren is at `open`, as offsets
|
|
759
|
+
* into `text` (exclusive of both parens). Null when unbalanced. */
|
|
760
|
+
function callArgumentSpan(text: string, open: number): { start: number; end: number } | null {
|
|
761
|
+
let depth = 0;
|
|
762
|
+
for (let index = open; index < text.length; index++) {
|
|
763
|
+
const ch = text[index];
|
|
764
|
+
if (ch === '(' || ch === '[' || ch === '{') depth++;
|
|
765
|
+
else if (ch === ')' || ch === ']' || ch === '}') {
|
|
766
|
+
depth--;
|
|
767
|
+
if (depth === 0) return { start: open + 1, end: index };
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/** The offset of the last TOP-LEVEL argument inside `[start,end)`, or null when
|
|
774
|
+
* the call has one argument (or none). Brackets/braces/parens/quotes are
|
|
775
|
+
* tracked so a comma inside an arrow body or an array never splits a call. */
|
|
776
|
+
function lastArgumentOffset(text: string, start: number, end: number): number | null {
|
|
777
|
+
let depth = 0;
|
|
778
|
+
let quote: string | null = null;
|
|
779
|
+
let lastComma = -1;
|
|
780
|
+
for (let index = start; index < end; index++) {
|
|
781
|
+
const ch = text[index]!;
|
|
782
|
+
if (quote) {
|
|
783
|
+
if (ch === '\\') index++;
|
|
784
|
+
else if (ch === quote) quote = null;
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
if (ch === "'" || ch === '"' || ch === '`') quote = ch;
|
|
788
|
+
else if (ch === '(' || ch === '[' || ch === '{') depth++;
|
|
789
|
+
else if (ch === ')' || ch === ']' || ch === '}') depth--;
|
|
790
|
+
else if (ch === ',' && depth === 0) lastComma = index;
|
|
791
|
+
}
|
|
792
|
+
return lastComma === -1 ? null : lastComma + 1;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function checkUseFramePriority(): void {
|
|
796
|
+
const useFrameRe = /\buseFrame\s*\(/g;
|
|
797
|
+
// A callback that draws the scene is the DOCUMENTED reason to pass a
|
|
798
|
+
// positive priority, and it is visible in the call itself:
|
|
799
|
+
// `useFrame(({gl, scene, camera}) => gl.render(scene, camera), 1)`.
|
|
800
|
+
const takesOverRenderingRe = /\.\s*render\s*\(/;
|
|
801
|
+
for (const file of srcFiles) {
|
|
802
|
+
const { rawLines, blanked } = readFileText(file);
|
|
803
|
+
for (const match of blanked.matchAll(useFrameRe)) {
|
|
804
|
+
const open = match.index + match[0].length - 1;
|
|
805
|
+
const span = callArgumentSpan(blanked, open);
|
|
806
|
+
if (!span) continue;
|
|
807
|
+
const priorityAt = lastArgumentOffset(blanked, span.start, span.end);
|
|
808
|
+
if (priorityAt === null) continue;
|
|
809
|
+
const priorityText = blanked.slice(priorityAt, span.end);
|
|
810
|
+
// A non-literal priority (`useFrame(cb, order)`) is beyond a line
|
|
811
|
+
// scanner's honesty budget — this rule stays quiet rather than guess.
|
|
812
|
+
const literal = priorityText.match(/^\s*(\d+(?:\.\d+)?)\s*$/);
|
|
813
|
+
if (!literal || Number.parseFloat(literal[1]!) <= 0) continue;
|
|
814
|
+
if (takesOverRenderingRe.test(blanked.slice(span.start, span.end))) continue;
|
|
815
|
+
addFinding(
|
|
816
|
+
file,
|
|
817
|
+
rawLines,
|
|
818
|
+
lineNumberAt(blanked, priorityAt + (priorityText.length - priorityText.trimStart().length)),
|
|
819
|
+
'W8',
|
|
820
|
+
'warn',
|
|
821
|
+
'useframe-priority-render-ownership',
|
|
822
|
+
`useFrame priority ${literal[1]} is positive — react-three-fiber reads that as "this callback owns rendering" and stops calling gl.render itself. ` +
|
|
823
|
+
'The canvas goes blank while state, mounts and logs all stay healthy.',
|
|
824
|
+
'Drop the priority (0 is the default) or make it NEGATIVE to order this callback before the render — negatives sequence, they never disable the render. ' +
|
|
825
|
+
'Keep a positive priority only if this callback draws the scene itself (gl.render(scene, camera)); if it renders indirectly, suppress with "// idioms-ignore W8 <reason>".',
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// ---------------------------------------------------------------------------
|
|
832
|
+
// W9 — the game has never been played
|
|
833
|
+
// ---------------------------------------------------------------------------
|
|
834
|
+
|
|
835
|
+
/** The one way a project shows that Play has actually run: `logs/play-*.jsonl`
|
|
836
|
+
* — the editor server opens one per Play session
|
|
837
|
+
* (packages/editor/server/editor-server.ts, POST /__editor/log-session).
|
|
838
|
+
* A working-tree fact, not a git fact (`logs/` is gitignored), written by
|
|
839
|
+
* the tool itself, so it cannot be produced by intending to play. This
|
|
840
|
+
* receipt is also the playtest's proof (owner ruling 2026-08-21). */
|
|
841
|
+
function hasPlayEvidence(): boolean {
|
|
842
|
+
const logsDir = join(rootDir, 'logs');
|
|
843
|
+
if (!existsSync(logsDir)) return false;
|
|
844
|
+
try {
|
|
845
|
+
return readdirSync(logsDir).some((f) => f.startsWith('play-') && f.endsWith('.jsonl'));
|
|
846
|
+
} catch {
|
|
847
|
+
return false; // unreadable logs dir is not evidence
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/** The scaffolder's own record of where this project came from
|
|
852
|
+
* (packages/create-vgai-project/src/baseline.ts). W9's history ANCHOR.
|
|
853
|
+
*
|
|
854
|
+
* W7 anchors its window on a file's last commit; W9 does the same, and this
|
|
855
|
+
* is the file that means "this project began here". Anchoring matters: an
|
|
856
|
+
* ABSOLUTE count of gameplay commits is only this game's history when the
|
|
857
|
+
* project owns the repository. Measured on this engine checkout, an
|
|
858
|
+
* unanchored count reads 70 gameplay commits for the starter template and 61
|
|
859
|
+
* for examples/top-down-strategy — real commits, but the ENGINE's
|
|
860
|
+
* maintenance history, and a rule that shouts at our own reference material
|
|
861
|
+
* on every run is noise, not a tripwire. A project with no committed
|
|
862
|
+
* baseline cannot be asked when it started, so W9 stays silent there. */
|
|
863
|
+
const SCAFFOLD_BASELINE_SOURCE = '.vgai/scaffold-baseline.json';
|
|
864
|
+
|
|
865
|
+
/** Gameplay commits since the scaffold anchor, at or above which a project
|
|
866
|
+
* that has never been played is reported.
|
|
867
|
+
*
|
|
868
|
+
* Why 5, and why this shape: the doctrine is "author in playable slices —
|
|
869
|
+
* enter Play and drive each slice before writing the next", so the honest
|
|
870
|
+
* question is "how many slices went by unplayed?" and a commit touching
|
|
871
|
+
* GAMEPLAY_PATHS is the cheapest available proxy for a slice. One or two
|
|
872
|
+
* must stay quiet — the first commits after a scaffold are often plumbing
|
|
873
|
+
* (an asset drop, a manifest edit, a rename) with nothing to look at yet.
|
|
874
|
+
* Five is deliberately the same number W7 uses for the same reason, and it
|
|
875
|
+
* is an order of magnitude below the measured failure this exists to catch:
|
|
876
|
+
* in the blind-probe build that motivated the rule, the agent spent 59
|
|
877
|
+
* minutes and 187 tool calls — 60% of its session — writing the whole game
|
|
878
|
+
* before its first Play, and 4 of its 5 worst bugs were pixel-only, every
|
|
879
|
+
* one of them found only afterwards. Reuses W7's GAMEPLAY_PATHS on purpose:
|
|
880
|
+
* "which files are the game" is one question, and it should not get two
|
|
881
|
+
* answers in one file. */
|
|
882
|
+
const UNPLAYED_GAMEPLAY_COMMIT_THRESHOLD = 5;
|
|
883
|
+
|
|
884
|
+
function checkPlayedBeforeAuthoring(): void {
|
|
885
|
+
if (hasPlayEvidence()) return; // already answered — and answered without spawning git
|
|
886
|
+
if (!isGitWorkTree()) return; // not a repo: silent
|
|
887
|
+
const scaffoldCommit = git(['log', '-1', '--format=%H', '--', SCAFFOLD_BASELINE_SOURCE]);
|
|
888
|
+
if (!scaffoldCommit) return; // no commits, or no baseline anchor: silent
|
|
889
|
+
const raw = git(['rev-list', '--count', `${scaffoldCommit}..HEAD`, '--', ...GAMEPLAY_PATHS]);
|
|
890
|
+
if (raw === null) return;
|
|
891
|
+
const gameplayCommits = Number.parseInt(raw, 10);
|
|
892
|
+
if (!Number.isFinite(gameplayCommits) || gameplayCommits < UNPLAYED_GAMEPLAY_COMMIT_THRESHOLD)
|
|
893
|
+
return;
|
|
894
|
+
addProjectFinding(
|
|
895
|
+
'W9',
|
|
896
|
+
'warn',
|
|
897
|
+
'author-in-playable-slices',
|
|
898
|
+
`${gameplayCommits} commit(s) have touched gameplay source (${GAMEPLAY_PATHS.join(', ')}) ` +
|
|
899
|
+
'since this project was scaffolded, and nothing here shows Play has EVER run — no ' +
|
|
900
|
+
'logs/play-*.jsonl receipt. A game written blind fails in ways ' +
|
|
901
|
+
'typecheck and unit tests cannot see: a blank canvas, an invisible mesh, a camera inside ' +
|
|
902
|
+
'the geometry.',
|
|
903
|
+
'Author in playable slices (AGENTS.md) — enter Play before writing the next slice: run ' +
|
|
904
|
+
'`npm run vgai -- play`, look at the game, playtest live, then continue; reading the ' +
|
|
905
|
+
'code again does not count.',
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// ---------------------------------------------------------------------------
|
|
910
|
+
// E5 — raw Math.random()/Date.now()/performance.now() in src/scripts/**,
|
|
911
|
+
// only when THIS project's own vgai.project.json declares
|
|
912
|
+
// determinism.seededRandom (D15, T-D15.3's third enforcer leg).
|
|
913
|
+
// ---------------------------------------------------------------------------
|
|
914
|
+
|
|
915
|
+
/** Reads rootDir/vgai.project.json and reports whether it declares
|
|
916
|
+
* `determinism.seededRandom: true`. Missing/malformed manifests read as
|
|
917
|
+
* "not declared" (silent) — validating the manifest's own shape is
|
|
918
|
+
* validate-manifest.ts's job, not this scanner's. */
|
|
919
|
+
function projectDeclaresSeededRandom(): boolean {
|
|
920
|
+
const manifestPath = join(rootDir, 'vgai.project.json');
|
|
921
|
+
if (!existsSync(manifestPath)) return false;
|
|
922
|
+
try {
|
|
923
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8')) as {
|
|
924
|
+
determinism?: { seededRandom?: boolean };
|
|
925
|
+
};
|
|
926
|
+
return manifest.determinism?.seededRandom === true;
|
|
927
|
+
} catch {
|
|
928
|
+
return false;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function checkDeterminismRng(): void {
|
|
933
|
+
if (!projectDeclaresSeededRandom()) return; // undeclared project: no contract, no findings (T4.1)
|
|
934
|
+
const rawRngRe = /\b(Math\.random|Date\.now|performance\.now)\s*\(/g;
|
|
935
|
+
const scriptFiles = srcFiles.filter((f) => relPath(f).startsWith('src/scripts/'));
|
|
936
|
+
for (const file of scriptFiles) {
|
|
937
|
+
const { rawLines, blanked } = readFileText(file);
|
|
938
|
+
for (const m of blanked.matchAll(rawRngRe)) {
|
|
939
|
+
const lineNo = lineNumberAt(blanked, m.index);
|
|
940
|
+
addFinding(
|
|
941
|
+
file,
|
|
942
|
+
rawLines,
|
|
943
|
+
lineNo,
|
|
944
|
+
'E5',
|
|
945
|
+
'error',
|
|
946
|
+
'gameplay-rng-ctx-random',
|
|
947
|
+
`raw "${m[1]}(" in src/scripts/** — this project's vgai.project.json declares ` +
|
|
948
|
+
'determinism.seededRandom, so ALL gameplay RNG/wall-clock reads must flow through ' +
|
|
949
|
+
'ctx.random, not raw Math.random()/Date.now()/performance.now().',
|
|
950
|
+
'Use ctx.random() (or ctx.random.stream(name) for an independent named stream) instead.',
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// ---------------------------------------------------------------------------
|
|
957
|
+
// E6 — starter contribution obligation left in place
|
|
958
|
+
// ---------------------------------------------------------------------------
|
|
959
|
+
|
|
960
|
+
function checkUnconfiguredProjectTools(): void {
|
|
961
|
+
const sentinel = /\bVGAI_STUB_UNIMPLEMENTED\b/g;
|
|
962
|
+
const toolFiles = srcFiles.filter((file) => /^(src\/tools|src\/contributions)\//.test(relPath(file)));
|
|
963
|
+
for (const file of toolFiles) {
|
|
964
|
+
const { rawLines, blanked } = readFileText(file);
|
|
965
|
+
for (const match of blanked.matchAll(sentinel)) {
|
|
966
|
+
addFinding(
|
|
967
|
+
file,
|
|
968
|
+
rawLines,
|
|
969
|
+
lineNumberAt(blanked, match.index),
|
|
970
|
+
'E6',
|
|
971
|
+
'error',
|
|
972
|
+
'studio-surfaces-complete',
|
|
973
|
+
'VGAI_STUB_UNIMPLEMENTED remains in an editor contribution — this is an explicit starter obligation, not a usable game-specific surface.',
|
|
974
|
+
"Rewrite the stub whole in this game's vocabulary and remove its warning. If this is the Data stub and the game genuinely has no authored content tables, delete the file.",
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// ---------------------------------------------------------------------------
|
|
981
|
+
// E9/E10 — the required floor for shipped 3D assets, and when it BINDS
|
|
982
|
+
// ---------------------------------------------------------------------------
|
|
983
|
+
|
|
984
|
+
/** Always WARN: the committed-route ship gate died with the route registry
|
|
985
|
+
* (owner ruling 2026-08-21 — pre-written assertions, including routes, enter
|
|
986
|
+
* the endless rewrite cycle). The naming floor stays a standing count the
|
|
987
|
+
* operator reads; handoff judgment is the DEVLOG'd playtest, not a rule
|
|
988
|
+
* escalation. */
|
|
989
|
+
function floorSeverity(): Severity {
|
|
990
|
+
return 'warn';
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// ---------------------------------------------------------------------------
|
|
994
|
+
// E9 — R3F authoring warnings (R3F00x) must be ZERO at handoff
|
|
995
|
+
// ---------------------------------------------------------------------------
|
|
996
|
+
|
|
997
|
+
interface R3fDiagnostic {
|
|
998
|
+
code: string;
|
|
999
|
+
line: number;
|
|
1000
|
+
component: string;
|
|
1001
|
+
message: string;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
type R3fAnalyzer = (
|
|
1005
|
+
code: string,
|
|
1006
|
+
file: string,
|
|
1007
|
+
opts?: { knownThreeSurface?: boolean },
|
|
1008
|
+
) => R3fDiagnostic[];
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* The editor's OWN R3F source analyzer, resolved from this project's
|
|
1012
|
+
* `@editor/*` tsconfig mapping and loaded synchronously.
|
|
1013
|
+
*
|
|
1014
|
+
* Why not re-implement the codes here: they are a real TypeScript AST pass
|
|
1015
|
+
* over the project's import graph (relative imports, re-exports, shared prop
|
|
1016
|
+
* aliases, simulation-owned roots, keyed list items), and a second regex
|
|
1017
|
+
* approximation living in this file would disagree with the editor's panels
|
|
1018
|
+
* about the same source — which is worse than no rule. The pass itself is
|
|
1019
|
+
* pure (`(code, file) => diagnostics`, `typescript` + `node:fs` and nothing
|
|
1020
|
+
* else), so nothing about reusing it needs a server or a session.
|
|
1021
|
+
*
|
|
1022
|
+
* The mapping is read from the project's own `tsconfig.json` instead of
|
|
1023
|
+
* hardcoding a path because it is exactly what the scaffolder rewrites per
|
|
1024
|
+
* dependency mode: an in-checkout project points at the engine checkout's
|
|
1025
|
+
* `packages/editor/src`, an installed one at
|
|
1026
|
+
* `node_modules/@volter/editor-core/src` (the published package ships `src/`).
|
|
1027
|
+
* Reading the alias means one code path serves both without knowing which.
|
|
1028
|
+
* A node_modules target uses Node's package resolution as a fallback so the
|
|
1029
|
+
* same mapping also works when a workspace manager hoists the package.
|
|
1030
|
+
*
|
|
1031
|
+
* Returns null when the analyzer genuinely cannot be reached. E9 reports that
|
|
1032
|
+
* as a finding of its own rather than passing silently — a floor rule that
|
|
1033
|
+
* disappears when its instrument is missing reads as a clean bill of health,
|
|
1034
|
+
* which is the one thing an analyzer must never produce.
|
|
1035
|
+
*/
|
|
1036
|
+
function resolveEditorModule(relative: string): string | null {
|
|
1037
|
+
const tsconfig = join(rootDir, 'tsconfig.json');
|
|
1038
|
+
if (!existsSync(tsconfig)) return null;
|
|
1039
|
+
let mapped: string;
|
|
1040
|
+
try {
|
|
1041
|
+
const raw = readFileSync(tsconfig, 'utf-8');
|
|
1042
|
+
// Deliberately a text match, not JSON.parse: tsconfigs legitimately carry
|
|
1043
|
+
// comments, and this needs one string out of a known key.
|
|
1044
|
+
const alias = raw.match(/"@editor\/\*"\s*:\s*\[\s*"([^"]+)"/);
|
|
1045
|
+
if (!alias) return null;
|
|
1046
|
+
const baseUrl = raw.match(/"baseUrl"\s*:\s*"([^"]+)"/)?.[1] ?? '.';
|
|
1047
|
+
const aliasTarget = alias[1]!.replace('*', relative);
|
|
1048
|
+
mapped = resolve(rootDir, baseUrl, aliasTarget);
|
|
1049
|
+
if (!existsSync(mapped)) {
|
|
1050
|
+
const nodeModulesMarker = 'node_modules/';
|
|
1051
|
+
const normalizedTarget = aliasTarget.replaceAll('\\', '/');
|
|
1052
|
+
const markerIndex = normalizedTarget.lastIndexOf(nodeModulesMarker);
|
|
1053
|
+
if (markerIndex < 0) return null;
|
|
1054
|
+
const packageTarget = normalizedTarget.slice(markerIndex + nodeModulesMarker.length);
|
|
1055
|
+
mapped = createRequire(join(rootDir, 'package.json')).resolve(packageTarget);
|
|
1056
|
+
}
|
|
1057
|
+
} catch {
|
|
1058
|
+
return null;
|
|
1059
|
+
}
|
|
1060
|
+
return existsSync(mapped) ? mapped : null;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function loadR3fAnalyzer(): R3fAnalyzer | null {
|
|
1064
|
+
const mapped = resolveEditorModule('ui-source/r3f-project-contracts.ts');
|
|
1065
|
+
if (!mapped) return null;
|
|
1066
|
+
try {
|
|
1067
|
+
const loaded = createRequire(import.meta.url)(mapped) as {
|
|
1068
|
+
r3fAuthoringDiagnostics?: unknown;
|
|
1069
|
+
};
|
|
1070
|
+
return typeof loaded.r3fAuthoringDiagnostics === 'function'
|
|
1071
|
+
? (loaded.r3fAuthoringDiagnostics as R3fAnalyzer)
|
|
1072
|
+
: null;
|
|
1073
|
+
} catch {
|
|
1074
|
+
return null;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* WHICH files the R3F floor applies to: the ones this project's own `three`
|
|
1080
|
+
* REGION owns. The analyzer stopped guessing that from a file's text — a
|
|
1081
|
+
* project that centralizes its `@react-three/fiber` imports had almost every
|
|
1082
|
+
* component silently skipped and reported as clean — so the answer comes from
|
|
1083
|
+
* the same resolver the editor uses: the manifest's roots, the adapter's
|
|
1084
|
+
* `include` globs, and this project's own import edges.
|
|
1085
|
+
*
|
|
1086
|
+
* Null means the resolver could not be reached, which E9 reports as a finding
|
|
1087
|
+
* of its own rather than analyzing nothing and calling it a pass.
|
|
1088
|
+
*/
|
|
1089
|
+
interface RegionSurfaces {
|
|
1090
|
+
surfaces: ReadonlyMap<string, string>;
|
|
1091
|
+
/** This project's `vgai.adapter.ts` declares a `regions` binding the static
|
|
1092
|
+
* reader could not evaluate, so its `include` globs were not consulted.
|
|
1093
|
+
* Carried rather than dropped: the editor's own loader EVALUATES that module
|
|
1094
|
+
* and would honor those globs, so silence here means the two readers
|
|
1095
|
+
* disagree with nobody told. */
|
|
1096
|
+
includesUnreadable: boolean;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function loadRegionSurfaces(): RegionSurfaces | null {
|
|
1100
|
+
const mapped = resolveEditorModule('asset-workflow/project-content.ts');
|
|
1101
|
+
if (!mapped) return null;
|
|
1102
|
+
try {
|
|
1103
|
+
const loaded = createRequire(import.meta.url)(mapped) as {
|
|
1104
|
+
projectFileSurfaces?: unknown;
|
|
1105
|
+
projectRegionEntriesFromSources?: unknown;
|
|
1106
|
+
};
|
|
1107
|
+
if (
|
|
1108
|
+
typeof loaded.projectFileSurfaces !== 'function' ||
|
|
1109
|
+
typeof loaded.projectRegionEntriesFromSources !== 'function'
|
|
1110
|
+
) {
|
|
1111
|
+
return null;
|
|
1112
|
+
}
|
|
1113
|
+
const read = (name: string): string => {
|
|
1114
|
+
try {
|
|
1115
|
+
return readFileSync(join(rootDir, name), 'utf-8');
|
|
1116
|
+
} catch {
|
|
1117
|
+
return '';
|
|
1118
|
+
}
|
|
1119
|
+
};
|
|
1120
|
+
const { regions, unreadable } = (
|
|
1121
|
+
loaded.projectRegionEntriesFromSources as (
|
|
1122
|
+
m: string,
|
|
1123
|
+
a: string,
|
|
1124
|
+
) => { regions: unknown[]; unreadable: boolean }
|
|
1125
|
+
)(read('vgai.project.json'), read('vgai.adapter.ts'));
|
|
1126
|
+
const sources = srcFiles.map((file) => ({
|
|
1127
|
+
path: relPath(file),
|
|
1128
|
+
source: readFileSync(file, 'utf-8'),
|
|
1129
|
+
}));
|
|
1130
|
+
const surfaces = (
|
|
1131
|
+
loaded.projectFileSurfaces as (
|
|
1132
|
+
files: unknown[],
|
|
1133
|
+
regions: unknown[],
|
|
1134
|
+
) => ReadonlyMap<string, string>
|
|
1135
|
+
)(sources, regions);
|
|
1136
|
+
return { surfaces, includesUnreadable: unreadable };
|
|
1137
|
+
} catch {
|
|
1138
|
+
return null;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function checkAuthoringWarnings(): void {
|
|
1143
|
+
const analyze = loadR3fAnalyzer();
|
|
1144
|
+
if (!analyze) {
|
|
1145
|
+
// Silent for a folder that is not a vgai project at all — this scanner
|
|
1146
|
+
// is pointed at scratch directories by its own tests and by curious
|
|
1147
|
+
// agents, and a rule about a missing tsconfig mapping is a rule about a
|
|
1148
|
+
// project. Inside a real project the mapping is scaffolded, so its
|
|
1149
|
+
// absence is worth saying out loud rather than passing quietly.
|
|
1150
|
+
if (!existsSync(join(rootDir, 'vgai.project.json'))) return;
|
|
1151
|
+
addProjectFinding(
|
|
1152
|
+
'E9',
|
|
1153
|
+
'warn',
|
|
1154
|
+
'authoring-warnings-zero',
|
|
1155
|
+
'the R3F authoring analyzer could not be loaded, so authoring warnings were NOT checked — this run says nothing about them. It resolves through this project\'s tsconfig.json "@editor/*" path mapping.',
|
|
1156
|
+
'Restore the "@editor/*" mapping in tsconfig.json (it points at the engine checkout, or at node_modules/@volter/editor-core/src) and re-run. `npm run vgai -- status` reports the same counts from the live editor meanwhile.',
|
|
1157
|
+
);
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
const resolved = loadRegionSurfaces();
|
|
1162
|
+
if (!resolved) {
|
|
1163
|
+
if (!existsSync(join(rootDir, 'vgai.project.json'))) return;
|
|
1164
|
+
addProjectFinding(
|
|
1165
|
+
'E9',
|
|
1166
|
+
'warn',
|
|
1167
|
+
'authoring-warnings-zero',
|
|
1168
|
+
"the region resolver could not be loaded, so no file could be attributed to this project's `three` region and authoring warnings were NOT checked — this run says nothing about them.",
|
|
1169
|
+
'Restore the "@editor/*" mapping in tsconfig.json (it points at the engine checkout, or at node_modules/@volter/editor-core/src) and re-run. `npm run vgai -- status` reports the same counts from the live editor meanwhile.',
|
|
1170
|
+
);
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
const { surfaces } = resolved;
|
|
1174
|
+
if (resolved.includesUnreadable) {
|
|
1175
|
+
addProjectFinding(
|
|
1176
|
+
'E9',
|
|
1177
|
+
'warn',
|
|
1178
|
+
'authoring-warnings-zero',
|
|
1179
|
+
"vgai.adapter.ts declares a `regions` binding that cannot be read statically, so its `include` globs were NOT consulted — any file those globs place is missing from this run, while the editor's own loader still honors them.",
|
|
1180
|
+
'Write the regions as an array of object literals passed directly to defineAdapter({…}), with literal `id` and `include` values (not a hoisted const, not a spread).',
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
const found: { file: string; diagnostic: R3fDiagnostic }[] = [];
|
|
1185
|
+
for (const file of srcFiles) {
|
|
1186
|
+
// The rules are about R3F source and would misfire on a `<div>`-rooted
|
|
1187
|
+
// component (R3F002 would tell it to forward `position`), so a file the
|
|
1188
|
+
// three region does not own is not analyzed. That is the resolver's call,
|
|
1189
|
+
// never this file's reading of the source.
|
|
1190
|
+
if (surfaces.get(relPath(file)) !== 'three') continue;
|
|
1191
|
+
let diagnostics: R3fDiagnostic[];
|
|
1192
|
+
try {
|
|
1193
|
+
diagnostics = analyze(readFileSync(file, 'utf-8'), resolve(file), {
|
|
1194
|
+
knownThreeSurface: true,
|
|
1195
|
+
});
|
|
1196
|
+
} catch {
|
|
1197
|
+
continue; // an unparseable file is tsc's finding to report, not this one
|
|
1198
|
+
}
|
|
1199
|
+
for (const diagnostic of diagnostics) found.push({ file, diagnostic });
|
|
1200
|
+
}
|
|
1201
|
+
if (found.length === 0) return;
|
|
1202
|
+
|
|
1203
|
+
const fix =
|
|
1204
|
+
'Fix each one at its callsite — the R3F00x codes are the named-nodes half of the required floor: a named prefab with a story (.agents/skills/vgai-3d-assets/SKILL.md). R3F004 wants an ordinary `name` prop, R3F002 wants the ThreeElements transform props forwarded to the one native root, R3F003 wants a single wrapping group, R3F005 wants runtime motion on an inner child. `idioms-ignore E9 <reason>` is the escape when a code is genuinely wrong about your source.';
|
|
1205
|
+
|
|
1206
|
+
if (floorSeverity() === 'warn') {
|
|
1207
|
+
const codes = [...new Set(found.map((f) => f.diagnostic.code))].sort();
|
|
1208
|
+
const files = [...new Set(found.map((f) => f.file))];
|
|
1209
|
+
addProjectFinding(
|
|
1210
|
+
'E9',
|
|
1211
|
+
'warn',
|
|
1212
|
+
'authoring-warnings-zero',
|
|
1213
|
+
`${found.length} R3F authoring warning(s) across ${files.length} file(s) — ${codes
|
|
1214
|
+
.map((code) => `${code}×${found.filter((f) => f.diagnostic.code === code).length}`)
|
|
1215
|
+
.join(
|
|
1216
|
+
', ',
|
|
1217
|
+
)}. Normal mid-build; the required floor is ZERO at handoff — the operator reads this count in every live playtest.`,
|
|
1218
|
+
fix,
|
|
1219
|
+
);
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
for (const { file, diagnostic } of found) {
|
|
1224
|
+
const { rawLines } = readFileText(file);
|
|
1225
|
+
addFinding(
|
|
1226
|
+
file,
|
|
1227
|
+
rawLines,
|
|
1228
|
+
diagnostic.line,
|
|
1229
|
+
'E9',
|
|
1230
|
+
'error',
|
|
1231
|
+
'authoring-warnings-zero',
|
|
1232
|
+
`${diagnostic.code}: ${diagnostic.message} Authoring warnings are ZERO at handoff — the required floor: a named prefab with a story (.agents/skills/vgai-3d-assets).`,
|
|
1233
|
+
fix,
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// ---------------------------------------------------------------------------
|
|
1239
|
+
// E10 — every shipped prefab has a colocated story
|
|
1240
|
+
// ---------------------------------------------------------------------------
|
|
1241
|
+
|
|
1242
|
+
/**
|
|
1243
|
+
* `src/prefabs/` and nothing wider, on purpose.
|
|
1244
|
+
*
|
|
1245
|
+
* The editor does NOT discover prefabs by path — a component becomes a
|
|
1246
|
+
* Content-tab prefab when a portable story names it as `meta.component`
|
|
1247
|
+
* (`AssetBrowser.tsx`), and that is deliberate: source shape alone cannot
|
|
1248
|
+
* tell a reusable prefab from scene composition or an implementation detail.
|
|
1249
|
+
* So there is exactly one place in a project where a file DECLARES itself a
|
|
1250
|
+
* shipped prefab without a story already existing, and it is this folder —
|
|
1251
|
+
* the scaffold's own prefab home and the path the 3D-asset floor names
|
|
1252
|
+
* (`src/prefabs/<Name>.tsx`). Widening the scan to `src/components/` or
|
|
1253
|
+
* `src/lib/` would demand stories from HUD connectors, dev-tools widgets and
|
|
1254
|
+
* singleton world entities that were never prefabs, which is how a floor rule
|
|
1255
|
+
* becomes noise nobody clears.
|
|
1256
|
+
*
|
|
1257
|
+
* The other direction matters just as much: residence in this folder is a
|
|
1258
|
+
* CLAIM. A prefab is what a designer STAMPS — selects, places, duplicates as
|
|
1259
|
+
* one piece, transform at the call site. If E10 fires on a file nobody would
|
|
1260
|
+
* stamp (a sky dome, a scene atmosphere, a one-per-scene singleton, dev
|
|
1261
|
+
* scenery), the finding is a MISCLASSIFICATION: move the file to
|
|
1262
|
+
* `src/components/` (environment infrastructure) or its lib — do NOT write a
|
|
1263
|
+
* story. A story for a non-placeable registers noise into the Content
|
|
1264
|
+
* palette, and clearing this rule with hollow stories is the exact failure
|
|
1265
|
+
* it exists to prevent (found live 2026-08-13: a project cleared three E10
|
|
1266
|
+
* findings by registering unused scaffold scenery into its palette).
|
|
1267
|
+
*/
|
|
1268
|
+
const PREFAB_DIR = 'src/prefabs/';
|
|
1269
|
+
const STORY_EXTENSIONS = ['.stories.tsx', '.stories.ts', '.stories.jsx', '.stories.js'] as const;
|
|
1270
|
+
|
|
1271
|
+
function prefabModules(): string[] {
|
|
1272
|
+
return srcFiles.filter((file) => {
|
|
1273
|
+
const rel = relPath(file);
|
|
1274
|
+
return (
|
|
1275
|
+
rel.startsWith(PREFAB_DIR) &&
|
|
1276
|
+
(rel.endsWith('.tsx') || rel.endsWith('.jsx')) &&
|
|
1277
|
+
!STORY_EXTENSIONS.some((extension) => rel.endsWith(extension))
|
|
1278
|
+
);
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
function prefabStory(file: string): string | undefined {
|
|
1283
|
+
const name = basename(file).replace(/\.[jt]sx$/, '');
|
|
1284
|
+
return STORY_EXTENSIONS.map((extension) => join(dirname(file), `${name}${extension}`)).find(
|
|
1285
|
+
(candidate) => existsSync(candidate),
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function hasRegisteredWorldPrefab(): boolean {
|
|
1290
|
+
return prefabModules().some((file) => {
|
|
1291
|
+
const name = basename(file).replace(/\.[jt]sx$/, '');
|
|
1292
|
+
const story = prefabStory(file);
|
|
1293
|
+
return (
|
|
1294
|
+
story !== undefined &&
|
|
1295
|
+
new RegExp(`\\bcomponent\\s*:\\s*${name}\\b`).test(readFileText(story).blanked)
|
|
1296
|
+
);
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
/** The line a prefab's own component is declared on — the honest place to
|
|
1301
|
+
* report, and the line an `idioms-ignore E10 <reason>` belongs on. */
|
|
1302
|
+
function componentDeclarationLine(blanked: string, name: string): number {
|
|
1303
|
+
const declaration = new RegExp(
|
|
1304
|
+
`\\bexport\\s+(?:default\\s+)?(?:async\\s+)?(?:function|const|class)\\s+${name}\\b`,
|
|
1305
|
+
).exec(blanked);
|
|
1306
|
+
return declaration ? lineNumberAt(blanked, declaration.index) : 1;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
function checkPrefabStories(): void {
|
|
1310
|
+
const prefabs = prefabModules();
|
|
1311
|
+
|
|
1312
|
+
for (const file of prefabs) {
|
|
1313
|
+
const name = basename(file).replace(/\.[jt]sx$/, '');
|
|
1314
|
+
const story = prefabStory(file);
|
|
1315
|
+
const { rawLines, blanked } = readFileText(file);
|
|
1316
|
+
const line = componentDeclarationLine(blanked, name);
|
|
1317
|
+
if (!story) {
|
|
1318
|
+
addFinding(
|
|
1319
|
+
file,
|
|
1320
|
+
rawLines,
|
|
1321
|
+
line,
|
|
1322
|
+
'E10',
|
|
1323
|
+
'error',
|
|
1324
|
+
'prefab-ships-a-story',
|
|
1325
|
+
`${name} is a shipped prefab with no colocated story — the required floor: a named prefab with a story (.agents/skills/vgai-3d-assets). Without one it is invisible to the editor's Content tab, which is where a designer finds, previews and places it.`,
|
|
1326
|
+
`Add ${relPath(file).replace(/\.[jt]sx$/, '.stories.tsx')} with \`component: ${name}\` in its \`meta\` — copy src/prefabs/HeroBox.stories.tsx and rename. If this module is not a shipped prefab, it does not belong in ${PREFAB_DIR}.`,
|
|
1327
|
+
);
|
|
1328
|
+
continue;
|
|
1329
|
+
}
|
|
1330
|
+
// The story IS the Content-tab registration only when its `meta` names
|
|
1331
|
+
// the component; a copy-pasted story still pointing at its source
|
|
1332
|
+
// component registers that one twice and this one not at all.
|
|
1333
|
+
if (!new RegExp(`\\bcomponent\\s*:\\s*${name}\\b`).test(readFileText(story).blanked)) {
|
|
1334
|
+
addFinding(
|
|
1335
|
+
file,
|
|
1336
|
+
rawLines,
|
|
1337
|
+
line,
|
|
1338
|
+
'E10',
|
|
1339
|
+
'error',
|
|
1340
|
+
'prefab-ships-a-story',
|
|
1341
|
+
`${relPath(story)} does not name ${name} as its \`meta.component\`, so ${name} is still unregistered — the story IS the Content-tab registration.`,
|
|
1342
|
+
`Set \`component: ${name}\` in that story's \`meta\`.`,
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// ---------------------------------------------------------------------------
|
|
1349
|
+
// W10 — a Three game should not silently lose the prefab architecture
|
|
1350
|
+
// ---------------------------------------------------------------------------
|
|
1351
|
+
|
|
1352
|
+
function manifestHasThreeRoot(): boolean {
|
|
1353
|
+
const manifestPath = join(rootDir, 'vgai.project.json');
|
|
1354
|
+
if (!existsSync(manifestPath)) return false;
|
|
1355
|
+
try {
|
|
1356
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8')) as {
|
|
1357
|
+
roots?: Array<{ adapter?: unknown }>;
|
|
1358
|
+
};
|
|
1359
|
+
return (manifest.roots ?? []).some((root) => root.adapter === 'three');
|
|
1360
|
+
} catch {
|
|
1361
|
+
return false;
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
function checkThreeProjectHasPrefab(): void {
|
|
1366
|
+
if (!manifestHasThreeRoot() || hasRegisteredWorldPrefab()) return;
|
|
1367
|
+
addProjectFinding(
|
|
1368
|
+
'W10',
|
|
1369
|
+
'warn',
|
|
1370
|
+
'three-project-has-prefab',
|
|
1371
|
+
'this project declares a Three root but has no correctly story-backed prefab. Deleting the starter subject must not silently collapse independently placeable entities into scenery or a scene monolith.',
|
|
1372
|
+
'For each object a designer should place, duplicate, or reuse as one piece, create src/prefabs/<Name>.tsx and a colocated story with `meta.component: <Name>`; keep owned construction pieces inside that prefab. If the world is genuinely fully procedural or every entity is simulation-owned, record that judgment in DEVLOG.md and suppress W10 with a reason in the relevant source.',
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// ---------------------------------------------------------------------------
|
|
1377
|
+
// E11 — scenes/ contains scenes, not things merely used by scenes
|
|
1378
|
+
// ---------------------------------------------------------------------------
|
|
1379
|
+
|
|
1380
|
+
const SCENE_DIR = 'src/scenes/';
|
|
1381
|
+
|
|
1382
|
+
function checkSceneFolderContract(): void {
|
|
1383
|
+
const sceneFiles = allFiles.filter((file) => relPath(file).startsWith(SCENE_DIR));
|
|
1384
|
+
for (const file of sceneFiles) {
|
|
1385
|
+
const rel = relPath(file);
|
|
1386
|
+
if (STORY_EXTENSIONS.some((extension) => rel.endsWith(extension))) {
|
|
1387
|
+
addFinding(
|
|
1388
|
+
file,
|
|
1389
|
+
readFileText(file).rawLines,
|
|
1390
|
+
1,
|
|
1391
|
+
'E11',
|
|
1392
|
+
'error',
|
|
1393
|
+
'scene-folder-means-scene',
|
|
1394
|
+
`${rel} is a story under ${SCENE_DIR}, but complete scenes are compositions, not Content prefabs.`,
|
|
1395
|
+
`Remove the Content story. If the component is independently placeable, move the component and its story to src/prefabs/ instead.`,
|
|
1396
|
+
);
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1399
|
+
if (!(rel.endsWith('.tsx') || rel.endsWith('.jsx'))) {
|
|
1400
|
+
addFinding(
|
|
1401
|
+
file,
|
|
1402
|
+
readFileText(file).rawLines,
|
|
1403
|
+
1,
|
|
1404
|
+
'E11',
|
|
1405
|
+
'error',
|
|
1406
|
+
'scene-folder-means-scene',
|
|
1407
|
+
`${rel} is support code under ${SCENE_DIR}, but that folder contains only complete JSX scene compositions.`,
|
|
1408
|
+
`Move this module to src/components/ by concern. If it has no runtime component, use the nearest owning src/lib/ or src/data/ module instead.`,
|
|
1409
|
+
);
|
|
1410
|
+
continue;
|
|
1411
|
+
}
|
|
1412
|
+
const sceneName = basename(file).replace(/\.[jt]sx$/, '');
|
|
1413
|
+
const { rawLines, blanked } = readFileText(file);
|
|
1414
|
+
if (!sceneName.endsWith('Scene')) {
|
|
1415
|
+
addFinding(
|
|
1416
|
+
file,
|
|
1417
|
+
rawLines,
|
|
1418
|
+
1,
|
|
1419
|
+
'E11',
|
|
1420
|
+
'error',
|
|
1421
|
+
'scene-folder-means-scene',
|
|
1422
|
+
`${rel} does not name a complete *Scene component. A file used by a scene is not therefore a scene.`,
|
|
1423
|
+
`Move non-placeable support to src/components/ by concern. If this is the complete composition, rename it to <Name>Scene.tsx.`,
|
|
1424
|
+
);
|
|
1425
|
+
continue;
|
|
1426
|
+
}
|
|
1427
|
+
const declaresScene = new RegExp(
|
|
1428
|
+
`\\bexport\\s+(?:default\\s+)?(?:async\\s+)?(?:function|const|class)\\s+${sceneName}\\b`,
|
|
1429
|
+
).test(blanked);
|
|
1430
|
+
if (!declaresScene) {
|
|
1431
|
+
addFinding(
|
|
1432
|
+
file,
|
|
1433
|
+
rawLines,
|
|
1434
|
+
1,
|
|
1435
|
+
'E11',
|
|
1436
|
+
'error',
|
|
1437
|
+
'scene-folder-means-scene',
|
|
1438
|
+
`${rel} is named like a scene but does not export ${sceneName}. The folder and filename must identify the actual complete composition, not disguise a helper.`,
|
|
1439
|
+
`Export the complete composition as ${sceneName}, or move this support module to src/components/ by concern.`,
|
|
1440
|
+
);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
// ---------------------------------------------------------------------------
|
|
1446
|
+
// Run
|
|
1447
|
+
// ---------------------------------------------------------------------------
|
|
1448
|
+
|
|
1449
|
+
checkPlaywrightImports();
|
|
1450
|
+
checkPackageJsonScripts();
|
|
1451
|
+
checkNoTestFurniture();
|
|
1452
|
+
checkWindowVgai();
|
|
1453
|
+
checkBotParam();
|
|
1454
|
+
checkPollingPump();
|
|
1455
|
+
checkColyseusStaticImport();
|
|
1456
|
+
checkTriggerNameIdentity();
|
|
1457
|
+
checkPlayedBeforeAuthoring();
|
|
1458
|
+
checkUseFramePriority();
|
|
1459
|
+
checkDeterminismRng();
|
|
1460
|
+
checkUnconfiguredProjectTools();
|
|
1461
|
+
checkAuthoringWarnings();
|
|
1462
|
+
checkPrefabStories();
|
|
1463
|
+
checkThreeProjectHasPrefab();
|
|
1464
|
+
checkSceneFolderContract();
|
|
1465
|
+
|
|
1466
|
+
const errorFindings = findings.filter((f) => f.severity === 'error');
|
|
1467
|
+
const warnFindings = findings.filter((f) => f.severity === 'warn');
|
|
1468
|
+
|
|
1469
|
+
/**
|
|
1470
|
+
* Leave the findings where a READER can find them before handoff.
|
|
1471
|
+
*
|
|
1472
|
+
* Measured failure: a blind probe ran `typecheck` seven times and this scanner
|
|
1473
|
+
* once, at minute 19, which turned every open finding into an end-of-build
|
|
1474
|
+
* scramble. `vgai status` — the command an agent polls constantly — reads this
|
|
1475
|
+
* file and surfaces the open ERROR findings as an advisory count, so they are
|
|
1476
|
+
* visible from the first status call while THIS script stays the gate.
|
|
1477
|
+
*
|
|
1478
|
+
* Machine-local and gitignored; best effort, because a scanner must not fail
|
|
1479
|
+
* on an unwritable cache directory.
|
|
1480
|
+
*/
|
|
1481
|
+
function writeFindingsRecord(): void {
|
|
1482
|
+
try {
|
|
1483
|
+
mkdirSync(join(rootDir, '.vgai'), { recursive: true });
|
|
1484
|
+
writeFileSync(
|
|
1485
|
+
join(rootDir, '.vgai', 'check-idioms.json'),
|
|
1486
|
+
`${JSON.stringify(
|
|
1487
|
+
{
|
|
1488
|
+
at: new Date().toISOString(),
|
|
1489
|
+
findings: findings.map((f) => ({
|
|
1490
|
+
rule: f.rule,
|
|
1491
|
+
idiom: f.idiom,
|
|
1492
|
+
severity: f.severity,
|
|
1493
|
+
file: f.file,
|
|
1494
|
+
line: f.line,
|
|
1495
|
+
})),
|
|
1496
|
+
},
|
|
1497
|
+
null,
|
|
1498
|
+
2,
|
|
1499
|
+
)}\n`,
|
|
1500
|
+
);
|
|
1501
|
+
} catch {
|
|
1502
|
+
// See this function's doc comment.
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
writeFindingsRecord();
|
|
1507
|
+
|
|
1508
|
+
function formatFinding(f: Finding): string {
|
|
1509
|
+
const where = f.line > 0 ? `${f.file}:${f.line}` : f.file;
|
|
1510
|
+
return ` ${where} [${f.severity.toUpperCase()}] ${f.rule} (${f.idiom})\n ${f.message}\n fix: ${f.fix}`;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
if (findings.length === 0) {
|
|
1514
|
+
console.log(`check-idioms: ${rootDir} — clean, 0 findings across ${codeFiles.length} file(s).`);
|
|
1515
|
+
process.exit(0);
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
if (errorFindings.length > 0) {
|
|
1519
|
+
console.error(`check-idioms: ${errorFindings.length} ERROR finding(s):\n`);
|
|
1520
|
+
for (const f of errorFindings) console.error(formatFinding(f));
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
if (warnFindings.length > 0) {
|
|
1524
|
+
console.error(
|
|
1525
|
+
`\ncheck-idioms: ${warnFindings.length} WARN finding(s)${strict ? ' (promoted to errors by --strict)' : ' (not failing without --strict)'}:\n`,
|
|
1526
|
+
);
|
|
1527
|
+
for (const f of warnFindings) console.error(formatFinding(f));
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
console.error(`\nSee IDIOMS.md for the full checklist these rules cover.`);
|
|
1531
|
+
|
|
1532
|
+
const failed = errorFindings.length > 0 || (strict && warnFindings.length > 0);
|
|
1533
|
+
process.exit(failed ? 1 : 0);
|