@woosh/meep-engine 2.168.0 → 2.169.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/build/bundle-worker-image-decoder.js +1 -1
- package/package.json +1 -1
- package/src/REVIEW_2026_08_06/adv-core.md +511 -0
- package/src/REVIEW_2026_08_06/adv-gfx.md +480 -0
- package/src/REVIEW_2026_08_06/adv-view.md +446 -0
- package/src/REVIEW_2026_08_06/api-core.md +296 -0
- package/src/REVIEW_2026_08_06/api-engine.md +195 -0
- package/src/REVIEW_2026_08_06/bvh-phys-sound-net.md +507 -0
- package/src/REVIEW_2026_08_06/core-binary.md +348 -0
- package/src/REVIEW_2026_08_06/core-collection.md +376 -0
- package/src/REVIEW_2026_08_06/core-math.md +221 -0
- package/src/REVIEW_2026_08_06/core-process.md +266 -0
- package/src/REVIEW_2026_08_06/core-science.md +223 -0
- package/src/REVIEW_2026_08_06/engine-ai-nav.md +279 -0
- package/src/REVIEW_2026_08_06/engine-animation.md +167 -0
- package/src/REVIEW_2026_08_06/engine-asset.md +166 -0
- package/src/REVIEW_2026_08_06/engine-ecs.md +920 -0
- package/src/REVIEW_2026_08_06/engine-input.md +197 -0
- package/src/REVIEW_2026_08_06/engine-misc.md +248 -0
- package/src/REVIEW_2026_08_06/engine-terrain.md +52 -0
- package/src/REVIEW_2026_08_06/generation.md +176 -0
- package/src/REVIEW_2026_08_06/geom-2d.md +434 -0
- package/src/REVIEW_2026_08_06/geom-3d.md +117 -0
- package/src/REVIEW_2026_08_06/gfx-gi.md +143 -0
- package/src/REVIEW_2026_08_06/gfx-render.md +740 -0
- package/src/REVIEW_2026_08_06/gfx-texture.md +690 -0
- package/src/REVIEW_2026_08_06/prior-criticals.md +49 -0
- package/src/REVIEW_2026_08_06/prior-highs.md +138 -0
- package/src/REVIEW_2026_08_06/test-gaps.md +364 -0
- package/src/REVIEW_2026_08_06/view.md +667 -0
- package/src/REVIEW_2026_08_06.md +571 -0
- package/src/core/binary/BinaryBuffer.d.ts.map +1 -1
- package/src/core/binary/BinaryBuffer.js +12 -1
- package/src/core/binary/BitImage2.d.ts +5 -1
- package/src/core/binary/BitImage2.d.ts.map +1 -1
- package/src/core/binary/BitImage2.js +5 -1
- package/src/core/bvh8/build/NodeProxy.d.ts.map +1 -1
- package/src/core/bvh8/build/NodeProxy.js +314 -308
- package/src/core/collection/list/List.d.ts +15 -0
- package/src/core/collection/list/List.d.ts.map +1 -1
- package/src/core/collection/list/List.js +34 -0
- package/src/core/collection/list/SortedListProjection.d.ts.map +1 -1
- package/src/core/collection/list/SortedListProjection.js +4 -1
- package/src/core/collection/map/BiMap.d.ts +25 -2
- package/src/core/collection/map/BiMap.d.ts.map +1 -1
- package/src/core/collection/map/BiMap.js +57 -3
- package/src/core/debug/matchers/IsAnything.d.ts.map +1 -1
- package/src/core/debug/matchers/IsAnything.js +5 -1
- package/src/core/events/signal/Signal.d.ts.map +1 -1
- package/src/core/events/signal/Signal.js +3 -1
- package/src/core/geom/2d/polygon/TRIANGULATION_DESIGN.md +433 -0
- package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.d.ts +31 -0
- package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.d.ts.map +1 -0
- package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.js +84 -0
- package/src/core/geom/2d/polygon/polygon2_signed_area.d.ts +24 -2
- package/src/core/geom/2d/polygon/polygon2_signed_area.d.ts.map +1 -1
- package/src/core/geom/2d/polygon/polygon2_signed_area.js +52 -34
- package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.d.ts +28 -0
- package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.d.ts.map +1 -0
- package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.js +86 -0
- package/src/core/geom/2d/polygon/polygon2_triangulate.d.ts +40 -0
- package/src/core/geom/2d/polygon/polygon2_triangulate.d.ts.map +1 -0
- package/src/core/geom/2d/polygon/polygon2_triangulate.js +1280 -0
- package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.d.ts +28 -0
- package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.d.ts.map +1 -0
- package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.js +77 -0
- package/src/core/geom/2d/triangle/tri2_rasterize_conservative.d.ts +6 -3
- package/src/core/geom/2d/triangle/tri2_rasterize_conservative.d.ts.map +1 -1
- package/src/core/geom/2d/triangle/tri2_rasterize_conservative.js +26 -13
- package/src/core/geom/2d/triangle/tri2_signed_area.d.ts +17 -1
- package/src/core/geom/2d/triangle/tri2_signed_area.d.ts.map +1 -1
- package/src/core/geom/2d/triangle/tri2_signed_area.js +35 -23
- package/src/core/geom/2d/v2_morton_encode.d.ts +20 -0
- package/src/core/geom/2d/v2_morton_encode.d.ts.map +1 -0
- package/src/core/geom/2d/v2_morton_encode.js +23 -0
- package/src/core/geom/3d/atlas/segment/atlas_merge_charts.d.ts.map +1 -1
- package/src/core/geom/3d/atlas/segment/atlas_merge_charts.js +4 -1
- package/src/core/geom/3d/polygon/polygon3_compute_normal.d.ts +27 -0
- package/src/core/geom/3d/polygon/polygon3_compute_normal.d.ts.map +1 -0
- package/src/core/geom/3d/polygon/polygon3_compute_normal.js +60 -0
- package/src/core/geom/3d/polygon/polygon3_triangulate.d.ts +34 -0
- package/src/core/geom/3d/polygon/polygon3_triangulate.d.ts.map +1 -0
- package/src/core/geom/3d/polygon/polygon3_triangulate.js +94 -0
- package/src/core/geom/3d/quaternion/quat3_multiply.d.ts +8 -2
- package/src/core/geom/3d/quaternion/quat3_multiply.d.ts.map +1 -1
- package/src/core/geom/3d/quaternion/quat3_multiply.js +8 -2
- package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/ConvexHullShape3D.js +32 -2
- package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.d.ts +23 -6
- package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.d.ts.map +1 -1
- package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.js +268 -124
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.d.ts +17 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.js +44 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.d.ts +20 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.js +40 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_closed.d.ts +19 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_closed.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_closed.js +34 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_manifold.d.ts +22 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_manifold.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_manifold.js +37 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.d.ts +87 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.js +599 -0
- package/src/core/geom/3d/triangle/tri3_compute_signed_volume.d.ts +32 -0
- package/src/core/geom/3d/triangle/tri3_compute_signed_volume.d.ts.map +1 -1
- package/src/core/geom/3d/triangle/tri3_compute_signed_volume.js +66 -7
- package/src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.d.ts +22 -1
- package/src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.d.ts.map +1 -1
- package/src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.js +83 -38
- package/src/core/geom/MANIFOLD_COMPARISON_2026_08_01.md +554 -0
- package/src/core/geom/vec3/v3_cotangent.d.ts +6 -2
- package/src/core/geom/vec3/v3_cotangent.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_cotangent.js +6 -2
- package/src/core/geom/vec3/v3_cross.d.ts +6 -2
- package/src/core/geom/vec3/v3_cross.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_cross.js +6 -2
- package/src/core/geom/vec3/v3_multiply.d.ts +6 -2
- package/src/core/geom/vec3/v3_multiply.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_multiply.js +6 -2
- package/src/core/geom/vec3/v3_subtract.d.ts +6 -2
- package/src/core/geom/vec3/v3_subtract.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_subtract.js +6 -2
- package/src/core/localization/Localization.js +1 -1
- package/src/core/math/random/randomSeed.d.ts +13 -0
- package/src/core/math/random/randomSeed.d.ts.map +1 -0
- package/src/core/math/random/randomSeed.js +16 -0
- package/src/core/math/spline/spline3_hermite_apply_transform.d.ts +50 -0
- package/src/core/math/spline/spline3_hermite_apply_transform.d.ts.map +1 -0
- package/src/core/math/spline/spline3_hermite_apply_transform.js +107 -0
- package/src/core/math/spline/spline3_hermite_reverse.d.ts +34 -0
- package/src/core/math/spline/spline3_hermite_reverse.d.ts.map +1 -0
- package/src/core/math/spline/spline3_hermite_reverse.js +49 -0
- package/src/core/math/statistics/hammersley_sequence.d.ts +17 -4
- package/src/core/math/statistics/hammersley_sequence.d.ts.map +1 -1
- package/src/core/math/statistics/hammersley_sequence.js +25 -16
- package/src/core/process/Future.d.ts.map +1 -1
- package/src/core/process/Future.js +12 -0
- package/src/core/process/PromiseWatcher.d.ts.map +1 -1
- package/src/core/process/PromiseWatcher.js +9 -2
- package/src/core/process/executor/ConcurrentExecutor.d.ts.map +1 -1
- package/src/core/process/executor/ConcurrentExecutor.js +12 -2
- package/src/core/process/worker/WorkerProxy.d.ts.map +1 -1
- package/src/core/process/worker/WorkerProxy.js +5 -2
- package/src/core/process/worker/extractTransferables.d.ts.map +1 -1
- package/src/core/process/worker/extractTransferables.js +4 -1
- package/src/engine/Engine.d.ts +12 -12
- package/src/engine/Engine.d.ts.map +1 -1
- package/src/engine/Engine.js +149 -10
- package/src/engine/EngineHarness.d.ts.map +1 -1
- package/src/engine/EngineHarness.js +4 -1
- package/src/engine/asset/loaders/ArrayBufferLoader.d.ts.map +1 -1
- package/src/engine/asset/loaders/ArrayBufferLoader.js +7 -0
- package/src/engine/ecs/speaker/lines/sets/LineSetDescription.d.ts.map +1 -1
- package/src/engine/ecs/speaker/lines/sets/LineSetDescription.js +3 -1
- package/src/engine/ecs/terrain/ecs/TerrainSystem.d.ts.map +1 -1
- package/src/engine/ecs/terrain/ecs/TerrainSystem.js +4 -1
- package/src/engine/ecs/terrain/tiles/TerrainTileManager.d.ts +29 -0
- package/src/engine/ecs/terrain/tiles/TerrainTileManager.d.ts.map +1 -1
- package/src/engine/ecs/terrain/tiles/TerrainTileManager.js +84 -23
- package/src/engine/graphics/CONTEXT_LOSS_RECOVERY_PLAN.md +446 -0
- package/src/engine/graphics/GraphicsEngine.d.ts +56 -7
- package/src/engine/graphics/GraphicsEngine.d.ts.map +1 -1
- package/src/engine/graphics/GraphicsEngine.js +196 -24
- package/src/engine/graphics/context/WebGLContextFailureReason.d.ts +9 -0
- package/src/engine/graphics/context/WebGLContextFailureReason.d.ts.map +1 -0
- package/src/engine/graphics/context/WebGLContextFailureReason.js +17 -0
- package/src/engine/graphics/context/WebGLContextMonitor.d.ts +106 -0
- package/src/engine/graphics/context/WebGLContextMonitor.d.ts.map +1 -0
- package/src/engine/graphics/context/WebGLContextMonitor.js +341 -0
- package/src/engine/graphics/context/WebGLContextState.d.ts +14 -0
- package/src/engine/graphics/context/WebGLContextState.d.ts.map +1 -0
- package/src/engine/graphics/context/WebGLContextState.js +24 -0
- package/src/engine/graphics/context/testWebGLContextLoss.d.ts +2 -0
- package/src/engine/graphics/context/testWebGLContextLoss.d.ts.map +1 -0
- package/src/engine/graphics/context/testWebGLContextLoss.js +696 -0
- package/src/engine/graphics/ecs/camera/serialization/CameraSerializationAdapter.d.ts.map +1 -1
- package/src/engine/graphics/ecs/camera/serialization/CameraSerializationAdapter.js +68 -32
- package/src/engine/graphics/ecs/camera/serialization/CameraSerializationUpgrader_0_1.d.ts +14 -0
- package/src/engine/graphics/ecs/camera/serialization/CameraSerializationUpgrader_0_1.d.ts.map +1 -0
- package/src/engine/graphics/ecs/camera/serialization/CameraSerializationUpgrader_0_1.js +41 -0
- package/src/engine/graphics/ecs/decal/v2/FPDecalSystem.js +1 -1
- package/src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.js +5 -0
- package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts +5 -1
- package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts.map +1 -1
- package/src/engine/graphics/ecs/trail2d/Trail2D.js +15 -0
- package/src/engine/graphics/ecs/trail2d/Trail2DSystem.d.ts +1 -1
- package/src/engine/graphics/ecs/trail2d/Trail2DSystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/trail2d/Trail2DSystem.js +2 -1
- package/src/engine/graphics/ecs/trail3d/Trail3D.d.ts +4 -1
- package/src/engine/graphics/ecs/trail3d/Trail3D.d.ts.map +1 -1
- package/src/engine/graphics/ecs/trail3d/Trail3D.js +488 -476
- package/src/engine/graphics/ecs/trail3d/Trail3DSystem.d.ts +1 -1
- package/src/engine/graphics/ecs/trail3d/Trail3DSystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/trail3d/Trail3DSystem.js +280 -253
- package/src/engine/graphics/geometry/MikkT/BuildNeighborsFast.d.ts.map +1 -1
- package/src/engine/graphics/geometry/MikkT/BuildNeighborsFast.js +3 -2
- package/src/engine/graphics/geometry/MikkT/GenerateSharedVerticesIndexList.d.ts.map +1 -1
- package/src/engine/graphics/geometry/MikkT/GenerateSharedVerticesIndexList.js +3 -1
- package/src/engine/graphics/geometry/computeMeshSurfaceArea.js +1 -1
- package/src/engine/graphics/load_and_set_cubemap_v0.d.ts +5 -0
- package/src/engine/graphics/load_and_set_cubemap_v0.d.ts.map +1 -1
- package/src/engine/graphics/load_and_set_cubemap_v0.js +26 -21
- package/src/engine/graphics/particles/particular/group/ParticleGroup.d.ts.map +1 -1
- package/src/engine/graphics/particles/particular/group/ParticleGroup.js +19 -8
- package/src/engine/graphics/particles/particular/group/optimizeCommandQueue.js +1 -1
- package/src/engine/graphics/texture/atlas/AtlasLookupTexture.d.ts.map +1 -1
- package/src/engine/graphics/texture/atlas/AtlasLookupTexture.js +10 -3
- package/src/engine/graphics/texture/atlas/TextureAtlas.d.ts.map +1 -1
- package/src/engine/graphics/texture/atlas/TextureAtlas.js +4 -1
- package/src/engine/graphics/texture/atlas/gpu/WebGLTextureAtlas.d.ts.map +1 -1
- package/src/engine/graphics/texture/atlas/gpu/WebGLTextureAtlas.js +11 -0
- package/src/engine/graphics/texture/cubemap/load_environment_map.d.ts.map +1 -1
- package/src/engine/graphics/texture/cubemap/load_environment_map.js +5 -2
- package/src/engine/graphics/texture/sampler/distance/computeSignedDistanceField_Chamfer.d.ts.map +1 -1
- package/src/engine/graphics/texture/sampler/distance/computeSignedDistanceField_Chamfer.js +26 -18
- package/src/engine/graphics/texture/sampler/filter/sampler2d_blur_gaussian.d.ts +15 -2
- package/src/engine/graphics/texture/sampler/filter/sampler2d_blur_gaussian.d.ts.map +1 -1
- package/src/engine/graphics/texture/sampler/filter/sampler2d_blur_gaussian.js +29 -3
- package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.d.ts +11 -1
- package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.d.ts.map +1 -1
- package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.js +158 -171
- package/src/engine/graphics/texture/sampler/sampler2d_paint.js +1 -1
- package/src/engine/graphics/texture/sampler/util/bitSet2Sampler2D.js +2 -2
- package/src/engine/input/ecs/util/TopDownCameraControllerHelper.d.ts.map +1 -1
- package/src/engine/input/ecs/util/TopDownCameraControllerHelper.js +10 -3
- package/src/engine/navigation/ecs/components/PathSerializationUpgrader_2_3.d.ts +14 -0
- package/src/engine/navigation/ecs/components/PathSerializationUpgrader_2_3.d.ts.map +1 -0
- package/src/engine/navigation/ecs/components/PathSerializationUpgrader_2_3.js +38 -0
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts.map +1 -1
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.js +433 -425
- package/src/engine/physics/constraint/solve_constraints.d.ts +4 -1
- package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -1
- package/src/engine/physics/constraint/solve_constraints.js +38 -13
- package/src/engine/physics/contact/ManifoldStore.d.ts +15 -3
- package/src/engine/physics/contact/ManifoldStore.d.ts.map +1 -1
- package/src/engine/physics/contact/ManifoldStore.js +15 -3
- package/src/engine/physics/ecs/Joint.d.ts +7 -4
- package/src/engine/physics/ecs/Joint.d.ts.map +1 -1
- package/src/engine/physics/ecs/Joint.js +7 -4
- package/src/engine/physics/ecs/PhysicsSystem.d.ts +14 -0
- package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
- package/src/engine/physics/ecs/PhysicsSystem.js +16 -0
- package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
- package/src/engine/physics/fluid/ecs/FluidSystem.d.ts +3 -3
- package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts +15 -7
- package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/convex_convex_manifold.js +33 -10
- package/src/engine/physics/narrowphase/refine_ray_concave.d.ts +6 -2
- package/src/engine/physics/narrowphase/refine_ray_concave.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/refine_ray_concave.js +6 -2
- package/src/engine/physics/narrowphase/refine_ray_hit.d.ts +6 -2
- package/src/engine/physics/narrowphase/refine_ray_hit.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/refine_ray_hit.js +6 -2
- package/src/engine/physics/queries/raycast.d.ts.map +1 -1
- package/src/engine/physics/queries/raycast.js +11 -4
- package/src/engine/save/storage/IndexedDBStorage.d.ts.map +1 -1
- package/src/engine/save/storage/IndexedDBStorage.js +21 -3
- package/src/engine/simulation/Ticker.d.ts +12 -0
- package/src/engine/simulation/Ticker.d.ts.map +1 -1
- package/src/engine/simulation/Ticker.js +18 -0
- package/src/engine/sound/simulation/AcousticSimulator.d.ts.map +1 -1
- package/src/engine/sound/simulation/AcousticSimulator.js +9 -1
- package/src/engine/sound/simulation/core/VolumeField.d.ts +6 -2
- package/src/engine/sound/simulation/core/VolumeField.d.ts.map +1 -1
- package/src/engine/sound/simulation/core/VolumeField.js +6 -2
- package/src/engine/sound/simulation/probe/AcousticProbeField.d.ts +6 -2
- package/src/engine/sound/simulation/probe/AcousticProbeField.d.ts.map +1 -1
- package/src/engine/sound/simulation/probe/AcousticProbeField.js +6 -2
- package/src/engine/sound/simulation/probe/acoustic_probe_transfer.d.ts +6 -2
- package/src/engine/sound/simulation/probe/acoustic_probe_transfer.d.ts.map +1 -1
- package/src/engine/sound/simulation/probe/acoustic_probe_transfer.js +6 -2
- package/src/engine/sound/simulation/probe/bakeProbeReflectors.d.ts +3 -1
- package/src/engine/sound/simulation/probe/bakeProbeReflectors.d.ts.map +1 -1
- package/src/engine/sound/simulation/probe/bakeProbeReflectors.js +3 -1
- package/src/engine/ui/GUIEngine.d.ts.map +1 -1
- package/src/engine/ui/GUIEngine.js +8 -1
- package/src/generation/theme/TerrainTheme.d.ts.map +1 -1
- package/src/generation/theme/TerrainTheme.js +2 -1
- package/src/generation/theme/ThemeEngine.d.ts.map +1 -1
- package/src/generation/theme/ThemeEngine.js +6 -5
- package/src/view/ViewGroup.d.ts.map +1 -1
- package/src/view/ViewGroup.js +9 -7
- package/src/view/graphics/WebGLContextFailureView.d.ts +19 -0
- package/src/view/graphics/WebGLContextFailureView.d.ts.map +1 -0
- package/src/view/graphics/WebGLContextFailureView.js +76 -0
- package/src/view/minimap/Minimap.d.ts +8 -0
- package/src/view/minimap/Minimap.d.ts.map +1 -1
- package/src/view/minimap/Minimap.js +16 -3
- package/src/view/minimap/dom/MinimapMarkerView.d.ts.map +1 -1
- package/src/view/minimap/dom/MinimapMarkerView.js +3 -1
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
# Correctness review — `core/collection`, `core/cache`, `core/events`, `core/fsm`, `core/function`, `core/IdPool.js`
|
|
2
|
+
|
|
3
|
+
Scope: every non-spec source file under the listed directories. Every finding below was reproduced
|
|
4
|
+
by executing the real modules through the project's jest harness (`npx jest --config jest.conf.json`
|
|
5
|
+
from `H:/git/moh`, with a throwaway spec under `app/src/`, since removed). Observed values quoted in
|
|
6
|
+
the failure scenarios are actual program output, not derivations.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## CRITICAL
|
|
11
|
+
|
|
12
|
+
### COLL-1 `SortedListProjection` inserts at the wrong index — output is never sorted
|
|
13
|
+
- **Severity**: CRITICAL
|
|
14
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/collection/list/SortedListProjection.js:47-51`
|
|
15
|
+
- **Defect**: `binarySearchLowIndex` returns *the index of the largest element `<= el`* (or `-1` when
|
|
16
|
+
`el` precedes everything). `handleAddition` passes that value straight to `List.insert`, so the
|
|
17
|
+
element is placed **before** its predecessor instead of after it. The `-1` case is worse: it reaches
|
|
18
|
+
`List.insert(-1, el)`, whose `else` branch runs `this.data.splice(-1, 0, el)` — a negative splice
|
|
19
|
+
index that inserts before the *last* element — and then dispatches `on.added` with index `-1`.
|
|
20
|
+
- **Failure scenario**:
|
|
21
|
+
```js
|
|
22
|
+
const input = new List();
|
|
23
|
+
const p = new SortedListProjection(input, (a, b) => a - b);
|
|
24
|
+
p.link();
|
|
25
|
+
input.add(3); input.add(5); input.add(7); input.add(4); input.add(1);
|
|
26
|
+
```
|
|
27
|
+
`p.output.asArray()` → `[5, 7, 4, 1, 3]` (measured). Expected `[1, 3, 4, 5, 7]`. The very first
|
|
28
|
+
two insertions happen to look right, which is why the defect hides in small cases.
|
|
29
|
+
- **Fix**: insert after the found floor index, and normalise the "before everything" result:
|
|
30
|
+
`const where = binarySearchLowIndex(this.output.data, el, this.comparator); this.output.insert(where + 1, el);`
|
|
31
|
+
(`-1 + 1 === 0` gives the correct front insert). Separately, `List.insert` should reject negative
|
|
32
|
+
indices rather than forwarding them to `splice`.
|
|
33
|
+
- **Confidence**: Confirmed
|
|
34
|
+
|
|
35
|
+
### COLL-2 `BiMap.add` writes both directions inverted — every accessor returns `undefined`
|
|
36
|
+
- **Severity**: CRITICAL
|
|
37
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/collection/map/BiMap.js:24-45`
|
|
38
|
+
- **Defect**: `add(key, value)` does `forward.set(value, key)` and `backward.set(key, value)`, i.e.
|
|
39
|
+
`forward` ends up keyed by *value* and `backward` by *key* — the opposite of the declared field
|
|
40
|
+
types (`forward: Map<K,V>`, `backward: Map<V,K>`) and of what `getValueByKey` / `getKeyByValue`
|
|
41
|
+
read. Both accessors, and therefore the `get`/`set` Map-compatibility aliases installed on the
|
|
42
|
+
prototype, miss on every entry.
|
|
43
|
+
- **Failure scenario**:
|
|
44
|
+
```js
|
|
45
|
+
const m = new BiMap();
|
|
46
|
+
m.set('a', 1);
|
|
47
|
+
m.get('a'); // undefined (expected 1)
|
|
48
|
+
m.getValueByKey('a'); // undefined (expected 1)
|
|
49
|
+
m.getKeyByValue(1); // undefined (expected 'a')
|
|
50
|
+
```
|
|
51
|
+
All three measured as `undefined`.
|
|
52
|
+
- **Fix**: swap the two writes in `add`: `this.forward.set(key, value); this.backward.set(value, key);`.
|
|
53
|
+
Note that the single in-repo consumer, `core/binary/EncodingBinaryBuffer.js` (`writeUTF8String` /
|
|
54
|
+
`readUTF8String`), currently compensates for the inversion by calling `add(stringValue, address)`
|
|
55
|
+
and then reading it back with `getKeyByValue(stringValue)` → address and `getValueByKey(address)` →
|
|
56
|
+
string. Its two `add(...)` call sites must have their arguments swapped in the same change,
|
|
57
|
+
otherwise fixing `BiMap` breaks string interning in the encoding buffer.
|
|
58
|
+
- **Confidence**: Confirmed
|
|
59
|
+
|
|
60
|
+
### COLL-3 `Signal.merge` forwards the `arguments` object instead of the arguments
|
|
61
|
+
- **Severity**: CRITICAL
|
|
62
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/events/signal/Signal.js:797-808`
|
|
63
|
+
- **Defect**: the bridging handler is `function handler() { result.dispatch(arguments); }`. `dispatch`
|
|
64
|
+
is variadic (`dispatch(...args)`), so the whole `arguments` object arrives as `args[0]`. Every
|
|
65
|
+
listener on a merged signal receives one array-like object rather than the original payload.
|
|
66
|
+
- **Failure scenario**:
|
|
67
|
+
```js
|
|
68
|
+
const a = new Signal(), b = new Signal();
|
|
69
|
+
const m = a.merge(b);
|
|
70
|
+
m.add((...args) => console.log(args));
|
|
71
|
+
a.send1(42);
|
|
72
|
+
```
|
|
73
|
+
Logged `[ { '0': 42 } ]` (measured); expected `[ 42 ]`.
|
|
74
|
+
- **Fix**: `function handler() { result.dispatch(...arguments); }` (or `result.dispatch.apply(result, arguments)`).
|
|
75
|
+
- **Confidence**: Confirmed
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## MAJOR
|
|
80
|
+
|
|
81
|
+
### COLL-4 `Deque.removeFirst` / `removeLast` on an empty deque corrupts head/tail/status
|
|
82
|
+
- **Severity**: MAJOR
|
|
83
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/collection/queue/Deque.js:321-327` and `:353-360`
|
|
84
|
+
- **Defect**: neither method checks `#status`. On an empty deque `removeFirst` still advances `#head`
|
|
85
|
+
past `#tail`, then `#reset_status(false)` sees `head !== tail` and sets `STATUS_NORMAL`. The deque
|
|
86
|
+
now reports itself as non-empty and `size()` computes `tail + capacity - head` — one short of the
|
|
87
|
+
whole backing array. `removeLast` fails symmetrically by moving `#tail` backwards.
|
|
88
|
+
- **Failure scenario**:
|
|
89
|
+
```js
|
|
90
|
+
const d = new Deque(); // default capacity 16
|
|
91
|
+
d.removeFirst(); // -> undefined, as documented by @returns {T|undefined}
|
|
92
|
+
d.isEmpty(); // false (measured; expected true)
|
|
93
|
+
d.size(); // 15 (measured; expected 0)
|
|
94
|
+
```
|
|
95
|
+
Any drain loop written against the documented `undefined` sentinel —
|
|
96
|
+
`while ((v = q.removeFirst()) !== undefined) {...}`, or the `pop()` alias in stack usage — leaves
|
|
97
|
+
the deque permanently poisoned: a following `while (!q.isEmpty()) q.removeFirst()` spins over 15
|
|
98
|
+
phantom `undefined` slots.
|
|
99
|
+
- **Fix**: return `undefined` immediately when `this.#status === STATUS_EMPTY` in both `removeFirst`
|
|
100
|
+
and `removeLast`, before touching `#head`/`#tail`.
|
|
101
|
+
- **Confidence**: Confirmed
|
|
102
|
+
|
|
103
|
+
### COLL-5 `BinaryHeap.pop` on an empty heap sets `length = -1`; the next `push` throws
|
|
104
|
+
- **Severity**: MAJOR
|
|
105
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/collection/heap/BinaryHeap.js:160-184`
|
|
106
|
+
- **Defect**: `pop()` decrements `this.length` unconditionally. On an empty heap `length` becomes
|
|
107
|
+
`-1`, `new_length !== 0` so the method falls through and writes `this.data[0] = undefined`,
|
|
108
|
+
re-growing the backing array. The subsequent `push` computes `position = this.length` (`-1`) and
|
|
109
|
+
calls `bubbleUp(-1)`, which reads `data[-1]` (`undefined`) and hands it to the score function.
|
|
110
|
+
- **Failure scenario**:
|
|
111
|
+
```js
|
|
112
|
+
const h = new BinaryHeap(x => x.score);
|
|
113
|
+
h.pop(); // -> undefined (behaviour asserted by BinaryHeap.spec.js:23 and :34)
|
|
114
|
+
h.size(); // -1 (measured)
|
|
115
|
+
h.isEmpty(); // false (measured)
|
|
116
|
+
h.push({score: 7});
|
|
117
|
+
// TypeError: Cannot read properties of undefined (reading 'score') (measured)
|
|
118
|
+
```
|
|
119
|
+
With a total score function such as `returnZero` there is no throw, but the pushed element is
|
|
120
|
+
silently lost (`size()` returns 0 afterwards). `BinaryHeap.spec.js:150` itself uses the
|
|
121
|
+
`while ((x = h.pop()) !== undefined)` drain pattern, so popping past empty is a sanctioned call.
|
|
122
|
+
- **Fix**: early-return `undefined` when `this.length === 0` at the top of `pop()`.
|
|
123
|
+
- **Confidence**: Confirmed
|
|
124
|
+
|
|
125
|
+
### COLL-6 `FilteredListProjection.apply` uses `invokeObjectEquals`, throwing on any element without `.equals`
|
|
126
|
+
- **Severity**: MAJOR
|
|
127
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/collection/list/FilteredListProjection.js:139`
|
|
128
|
+
- **Defect**: the diff uses `array_set_diff(oldOutput, newOutput, invokeObjectEquals)`, and
|
|
129
|
+
`invokeObjectEquals(a, b)` is an unguarded `a.equals(b)`. Any element type without an `equals`
|
|
130
|
+
method (numbers, strings, plain objects) throws. The throw is swallowed by `Signal`'s dispatch
|
|
131
|
+
`try/catch`, so the projection just stops updating and `output` silently goes stale forever.
|
|
132
|
+
`List.patch`, which performs the identical diff, correctly uses the guarded `objectsEqual`.
|
|
133
|
+
- **Failure scenario**:
|
|
134
|
+
```js
|
|
135
|
+
const input = new List([1, 2, 3]);
|
|
136
|
+
const p = new FilteredListProjection(input, new List());
|
|
137
|
+
p.deferred = false;
|
|
138
|
+
p.link(); // output === [1,2,3] — first apply is safe because oldOutput is empty
|
|
139
|
+
input.add(4); // second apply
|
|
140
|
+
```
|
|
141
|
+
Measured: `Failed to dispatch handler [Function: update] TypeError: a.equals is not a function`
|
|
142
|
+
at `invokeObjectEquals.js:9` via `array_set_diff.js:30`. `p.output` stays `[1,2,3]` with no
|
|
143
|
+
visible error to the caller.
|
|
144
|
+
- **Fix**: pass `objectsEqual` (from `core/function/objectsEqual.js`) instead of
|
|
145
|
+
`invokeObjectEquals`, matching `List.patch`.
|
|
146
|
+
- **Confidence**: Confirmed
|
|
147
|
+
|
|
148
|
+
### COLL-7 `SortedListProjection` never observes removals from the input list
|
|
149
|
+
- **Severity**: MAJOR
|
|
150
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/collection/list/SortedListProjection.js:33-41`
|
|
151
|
+
- **Defect**: `link()` subscribes only to `input.on.added`; `unlink()` unsubscribes only that. There
|
|
152
|
+
is no `on.removed` handler, so elements deleted from the input remain in `output` indefinitely.
|
|
153
|
+
This is independent of COLL-1 — fixing the insertion index alone still leaves the projection wrong.
|
|
154
|
+
- **Failure scenario**: with COLL-1 fixed, `input.add(3); input.add(5); input.remove(0);` leaves
|
|
155
|
+
`output` as `[3, 5]` while `input` is `[5]`. Measured on the current code: after
|
|
156
|
+
`input.add(3..1); input.remove(0)`, `output` was unchanged at `[5,7,4,1,3]`.
|
|
157
|
+
- **Fix**: in `link()` add `this.input.on.removed.add(this.handleRemoval, this)` with a handler that
|
|
158
|
+
locates the element in `output` (binary search on the comparator, then linear scan across the
|
|
159
|
+
equal-comparing run for identity) and removes it; mirror the unsubscribe in `unlink()`.
|
|
160
|
+
- **Confidence**: Confirmed
|
|
161
|
+
|
|
162
|
+
### COLL-8 `ArrayIteratorRandom.initialize` does not truncate the shuffle sequence
|
|
163
|
+
- **Severity**: MAJOR
|
|
164
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/collection/array/iterator/ArrayIteratorRandom.js:23-35`
|
|
165
|
+
- **Defect**: `initialize` writes `__sequence[i] = i` for `i < data.length` but never shrinks
|
|
166
|
+
`__sequence`. `array_shuffle` then permutes the full stale length and `next()` bounds its loop with
|
|
167
|
+
`sequence.length`, so re-initialising the (explicitly reusable, allocation-free) iterator with a
|
|
168
|
+
shorter array yields too many items — including `undefined` — and repeats real ones.
|
|
169
|
+
- **Failure scenario**:
|
|
170
|
+
```js
|
|
171
|
+
const it = new ArrayIteratorRandom();
|
|
172
|
+
it.initialize(['a','b','c']);
|
|
173
|
+
it.initialize(['x','y']);
|
|
174
|
+
// drain via next(result)
|
|
175
|
+
```
|
|
176
|
+
Yielded `["x", "y", undefined]` (measured) — three items from a two-element array.
|
|
177
|
+
- **Fix**: `this.__sequence.length = n;` after the fill loop (before `array_shuffle`), or pass the
|
|
178
|
+
explicit `length` argument that `array_shuffle` already supports and use it as the bound in `next`.
|
|
179
|
+
- **Confidence**: Confirmed
|
|
180
|
+
|
|
181
|
+
### COLL-9 `RowFirstTable.hash()` is inconsistent with `RowFirstTable.equals()`
|
|
182
|
+
- **Severity**: MAJOR
|
|
183
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/collection/table/RowFirstTable.js:121-150`
|
|
184
|
+
- **Defect**: `hash()` seeds from `this.data.byteLength` and taps across the whole *allocated buffer*,
|
|
185
|
+
including capacity beyond `length`. `equals()` compares only `length * bytesPerRecord` bytes. Two
|
|
186
|
+
tables that compare equal therefore hash differently whenever their capacities differ, breaking the
|
|
187
|
+
hash/equals contract for any `HashMap`/`HashSet`/`Cache` keyed by a table, and for hash-based
|
|
188
|
+
dirty-checking. (`hash()` also returns a non-int32 value because the accumulator is never masked.)
|
|
189
|
+
- **Failure scenario**:
|
|
190
|
+
```js
|
|
191
|
+
const spec = RowFirstTableSpec.get([BinaryDataType.Uint32]);
|
|
192
|
+
const a = new RowFirstTable(spec);
|
|
193
|
+
for (let i = 0; i < 20; i++) a.addRow([i]); // grows to capacity 52
|
|
194
|
+
const b = new RowFirstTable(spec);
|
|
195
|
+
b.copy(a);
|
|
196
|
+
b.trim(); // capacity 20
|
|
197
|
+
a.equals(b); // true (measured)
|
|
198
|
+
a.hash(); // -13796099888 (measured)
|
|
199
|
+
b.hash(); // 4190943312 (measured)
|
|
200
|
+
```
|
|
201
|
+
- **Fix**: derive the hash from the live region only — seed with `this.length` (and
|
|
202
|
+
`spec.bytesPerRecord`) and bound the tap loop by `this.length * spec.bytesPerRecord >> 2` instead of
|
|
203
|
+
`this.data.byteLength >> 2`. Also `|0` the accumulator so the result is a 32-bit integer.
|
|
204
|
+
- **Confidence**: Confirmed
|
|
205
|
+
|
|
206
|
+
### COLL-10 `serializeRowFirstTable` / `deserializeRowFirstTable` drop the spec's endianness
|
|
207
|
+
- **Severity**: MAJOR
|
|
208
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/collection/table/serializeRowFirstTable.js:8-27`,
|
|
209
|
+
`H:/git/moh/app/src/mir-engine/meep/src/core/collection/table/deserializeRowFirstTable.js:39`
|
|
210
|
+
- **Defect**: the serializer writes column types, record size and record count but not
|
|
211
|
+
`spec.endianType`. The deserializer rebuilds the spec with `RowFirstTableSpec.get(types)`, which
|
|
212
|
+
defaults to `EndianType.LittleEndian`. A table built on a big-endian spec round-trips with every
|
|
213
|
+
multi-byte cell byte-swapped. (`bytesPerRecord` is read and then discarded, so there is not even an
|
|
214
|
+
incidental mismatch check.)
|
|
215
|
+
- **Failure scenario**:
|
|
216
|
+
```js
|
|
217
|
+
const spec = new RowFirstTableSpec([BinaryDataType.Uint32], EndianType.BigEndian);
|
|
218
|
+
const t = new RowFirstTable(spec);
|
|
219
|
+
t.addRow([1]);
|
|
220
|
+
// serialize into a BinaryBuffer, rewind, deserialize into a fresh table
|
|
221
|
+
t2.readCellValue(0, 0); // 16777216 (measured; expected 1)
|
|
222
|
+
```
|
|
223
|
+
`t2.spec.endianType` came back as LittleEndian regardless of the source spec.
|
|
224
|
+
- **Fix**: write `spec.endianType` (one byte) after the type list in the serializer and pass it to
|
|
225
|
+
`RowFirstTableSpec.get(types, endianType)` on read. Bump/branch on a format marker if existing
|
|
226
|
+
payloads must stay readable.
|
|
227
|
+
- **Confidence**: Confirmed
|
|
228
|
+
|
|
229
|
+
### COLL-11 `FrequencySketch` keeps Caffeine's 16-counters-per-slot layout on a 32-bit table
|
|
230
|
+
- **Severity**: MAJOR
|
|
231
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/cache/FrequencySketch.js:43-68`, `:172-218`
|
|
232
|
+
- **Defect**: the port kept Caffeine's `long[]` addressing — `start = (hash & 3) << 2` with
|
|
233
|
+
`start + i` spanning `0..15`, and shifts of `(start + i) << 2` spanning `0..60` — but the table is a
|
|
234
|
+
`Uint32Array`, which holds only 8 four-bit counters per word. JavaScript takes shift counts mod 32,
|
|
235
|
+
so logical counters 8..15 silently alias counters 0..7 in both `incrementAt` (`0xF << offset`,
|
|
236
|
+
`1 << offset`) and `frequency` (`table[index] >>> ((start + i) << 2)`). Half the counter space is
|
|
237
|
+
unreachable and the documented width / error bound (`e / width`, 93.75% confidence) does not hold.
|
|
238
|
+
- **Failure scenario**:
|
|
239
|
+
```js
|
|
240
|
+
const s = new FrequencySketch();
|
|
241
|
+
s.ensureCapacity(1);
|
|
242
|
+
s.increment(0); // spread(0) & 3 === 0 -> logical counters 0..3
|
|
243
|
+
s.frequency(3); // spread(3) & 3 === 2 -> logical counters 8..11
|
|
244
|
+
```
|
|
245
|
+
Measured `s.frequency(3) === 1` (table word `0x1111`); the 16-counter layout the class documents
|
|
246
|
+
gives `0`. Element 3 was never incremented. This feeds `CacheWTinylfu.__admissionDuel` /
|
|
247
|
+
`shouldPromoteToProtected` directly, so admission and promotion decisions are made on inflated
|
|
248
|
+
frequencies.
|
|
249
|
+
- **Fix**: either restrict the layout to the 8 counters a `Uint32Array` actually has
|
|
250
|
+
(`start = (hash & 1) << 2`, `start + i` in `0..7`, `RESET_MASK`/`ONE_MASK` already correct for 32
|
|
251
|
+
bits), or keep 16 counters per logical slot by addressing a `Uint32Array` of `2 * length` as pairs
|
|
252
|
+
(word `= index * 2 + ((start + i) >> 3)`, shift `= ((start + i) & 7) << 2`). Update the class
|
|
253
|
+
docstring to match whichever is chosen.
|
|
254
|
+
- **Confidence**: Confirmed
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## MINOR
|
|
259
|
+
|
|
260
|
+
### COLL-12 `Signal.contains` ignores its documented "thisArg optional" contract
|
|
261
|
+
- **Severity**: MINOR
|
|
262
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/events/signal/Signal.js:101-113`
|
|
263
|
+
- **Defect**: the JSDoc states `@param {*} [thisArg] if not present, will not be considered`, but the
|
|
264
|
+
call is forwarded to `signal_handler_list_find`, which requires `n.context === thisArg`. With
|
|
265
|
+
`thisArg` omitted the comparison becomes `context === undefined`, so any handler registered with a
|
|
266
|
+
context reports as absent.
|
|
267
|
+
- **Failure scenario**: `const s = new Signal(); s.add(f, ctx); s.contains(f)` → `false` (measured),
|
|
268
|
+
while `s.contains(f, ctx)` → `true`. Callers using `contains` as an "is this function subscribed at
|
|
269
|
+
all?" guard will double-register.
|
|
270
|
+
- **Fix**: when `thisArg === undefined`, walk the chain matching on `handle` only (a dedicated
|
|
271
|
+
`signal_handler_list_find_by_handle`, mirroring the existing
|
|
272
|
+
`findSignalHandlerIndexByHandle`/`...AndContext` pair), or correct the JSDoc to state that the
|
|
273
|
+
context is always part of the match.
|
|
274
|
+
- **Confidence**: Confirmed
|
|
275
|
+
|
|
276
|
+
### COLL-13 `array_contains_duplicates` ignores `end_index` when scanning forward
|
|
277
|
+
- **Severity**: MINOR
|
|
278
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/collection/array/array_contains_duplicates.js:22`
|
|
279
|
+
- **Defect**: the outer loop respects `end_index`, but the inner probe is
|
|
280
|
+
`source.indexOf(element, k + 1)` with no upper bound, so it reports duplicates that lie outside the
|
|
281
|
+
requested `[start_index, end_index]` window. The parameters are documented as
|
|
282
|
+
"first/last index to be included in the search".
|
|
283
|
+
- **Failure scenario**: `array_contains_duplicates([1, 2, 1], 0, 1)` → `true` (measured). The window
|
|
284
|
+
`[0,1]` contains `[1, 2]`, which has no duplicates. The spec only exercises the whole-array default.
|
|
285
|
+
- **Fix**: bound the probe, e.g.
|
|
286
|
+
`const p = source.indexOf(element, k + 1); if (p !== -1 && p <= end_index) return true;`
|
|
287
|
+
- **Confidence**: Confirmed
|
|
288
|
+
|
|
289
|
+
### COLL-14 `weightedRandomFromArray` can return a zero-weight element
|
|
290
|
+
- **Severity**: MINOR
|
|
291
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/collection/array/weightedRandomFromArray.js:43-50`
|
|
292
|
+
- **Defect**: zero-weight entries leave `weight_total` unchanged, so the cumulative array repeats a
|
|
293
|
+
value. `binarySearchHighIndex` returns on exact equality, which selects the *first* index holding
|
|
294
|
+
that cumulative value — the zero-weight one. The intended semantics need the first index whose
|
|
295
|
+
cumulative weight is *strictly greater* than the target.
|
|
296
|
+
- **Failure scenario**:
|
|
297
|
+
`weightedRandomFromArray(['A','B'], () => 0, x => x === 'A' ? 0 : 1)` → `'A'` (measured), an element
|
|
298
|
+
whose weight is 0 and which should be unreachable. Triggered whenever the supplied `random` returns
|
|
299
|
+
exactly `0` (seeded generators commonly can) and the leading element has zero weight.
|
|
300
|
+
- **Fix**: after the binary search, advance past any run of equal cumulative values —
|
|
301
|
+
`while (index < length - 1 && scratch_weights[index] === scratch_weights[index + 1]) index++;` — or
|
|
302
|
+
use a dedicated upper-bound search that never short-circuits on equality.
|
|
303
|
+
- **Confidence**: Confirmed
|
|
304
|
+
|
|
305
|
+
### COLL-15 `sparse_typed_array_hash` silently ignores its `offset` parameter
|
|
306
|
+
- **Severity**: MINOR
|
|
307
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/collection/array/typed/sparse_typed_array_hash.js:13-25`
|
|
308
|
+
- **Defect**: the function takes `offset` but hard-codes `0` in the `computeStridedArrayHash` call, so
|
|
309
|
+
the hash always starts at index 0 regardless of the requested range.
|
|
310
|
+
- **Failure scenario**:
|
|
311
|
+
`sparse_typed_array_hash(new Uint8Array([9,1,2]), 1, 3)` → `79698` and
|
|
312
|
+
`sparse_typed_array_hash(new Uint8Array([7,1,2]), 1, 3)` → `81620` (both measured). The two arrays
|
|
313
|
+
are identical over the requested `[1,3)` range and must hash the same; they differ only at index 0,
|
|
314
|
+
which should have been skipped. All three in-repo callers pass `offset === 0`, so nothing live is
|
|
315
|
+
affected today — but this is public library surface.
|
|
316
|
+
- **Fix**: forward the parameter: `computeStridedArrayHash(array, offset, offset + hash_evaluation_length, stride, computeHashFloat)`
|
|
317
|
+
(`computeStridedArrayHash` treats its third argument as an exclusive end index), or drop the
|
|
318
|
+
parameter from the signature if a fixed prefix is genuinely intended.
|
|
319
|
+
- **Confidence**: Confirmed
|
|
320
|
+
|
|
321
|
+
### COLL-16 `uint8_array_hash` / `uint16_array_hash` mis-handle a non-zero `offset`
|
|
322
|
+
- **Severity**: MINOR
|
|
323
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/collection/array/typed/uint8_array_hash.js:13-33`,
|
|
324
|
+
`H:/git/moh/app/src/mir-engine/meep/src/core/collection/array/typed/uint16_array_hash.js:13-26`
|
|
325
|
+
- **Defect**: the head-alignment count is derived from `length` alone (`length & 3`, `length & 1`)
|
|
326
|
+
while the cursor starts at `offset`, and the main loop's bound is `i < length`. With `offset > 0`
|
|
327
|
+
the two disagree: the word loop reads `array[i+1..i+3]` past the requested end, so bytes outside
|
|
328
|
+
the range enter the hash (and out-of-range reads coerce to `0`). The sibling `uint32_array_hash`
|
|
329
|
+
and the only in-repo caller `array_buffer_hash` treat the third argument as an exclusive end index
|
|
330
|
+
and always pass `offset === 0`; under either reading (end index or count) the 8-bit and 16-bit
|
|
331
|
+
variants are wrong for `offset > 0`.
|
|
332
|
+
- **Failure scenario**:
|
|
333
|
+
`uint8_array_hash(new Uint8Array([0,0,1,2,3,7,7]), 2, 5)` → `117904870` and
|
|
334
|
+
`uint8_array_hash(new Uint8Array([0,0,1,2,3,8,8]), 2, 5)` → `134747622` (both measured). The
|
|
335
|
+
requested range `[2,5)` is `1,2,3` in both arrays, so the hashes must match; indices 5 and 6 leaked
|
|
336
|
+
in. The existing spec calls `uint8_array_hash(arr, 1, 3)` but only asserts the result is a finite
|
|
337
|
+
integer, so the defect is untested.
|
|
338
|
+
- **Fix**: compute the head remainder from the range size, not the total — `const n = length - offset;
|
|
339
|
+
const overflow = n & 3;` — and run the word loop while `i + 3 < length`, handling any tail
|
|
340
|
+
byte-wise. Same shape for the 16-bit variant (`(length - offset) & 1`, loop while `i + 1 < length`).
|
|
341
|
+
- **Confidence**: Confirmed
|
|
342
|
+
|
|
343
|
+
### COLL-17 `HashMap`/`HashSet`/`Cache` constructed with `capacity: 0` throw
|
|
344
|
+
- **Severity**: MINOR
|
|
345
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/collection/map/HashMap.js:224` and `:247, 270-290`
|
|
346
|
+
- **Defect**: the constructor explicitly permits `capacity === 0` (`assert.isNonNegativeInteger`), but
|
|
347
|
+
`ceilPowerOfTwo(0)` returns `0` and `#setBinCount(0)` computes `ctz32(0) === 32`, giving
|
|
348
|
+
`__entries_allocated_count = 2 ** 32`. In development the `greaterThanOrEqual(count, 1)` assert
|
|
349
|
+
fires; in a production bundle that assert is stripped and execution reaches
|
|
350
|
+
`UintArrayForCount(2 ** 32 + 2)`, which throws `Unsupported size 4294967298`. Either way an
|
|
351
|
+
otherwise-legal argument aborts construction, and it propagates to `HashSet` and `Cache`, both of
|
|
352
|
+
which forward `capacity` untouched.
|
|
353
|
+
- **Failure scenario**: `new HashMap({ capacity: 0 })` → thrown
|
|
354
|
+
`bucket count must be at least 1. Expected 0 >= 1.` (measured under the test harness);
|
|
355
|
+
`Unsupported size 4294967298` in a production build. Same for `new HashSet({capacity: 0})` and
|
|
356
|
+
`new Cache({capacity: 0})`.
|
|
357
|
+
- **Fix**: clamp in the constructor — `this.#setBinCount(ceilPowerOfTwo(max2(1, capacity)))` — so a
|
|
358
|
+
zero/undersized request lands on the minimum table instead of overflowing the shift.
|
|
359
|
+
- **Confidence**: Confirmed
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## Checked and found clean (no findings)
|
|
364
|
+
|
|
365
|
+
`IdPool`, `Uint32Map`, `PairUint32Map`, `Uint32MinHeap`, `Uint32Heap`, `Uint32Heap4`,
|
|
366
|
+
`IndexedFloatMinHeap` / `IndexedFloatMaxHeap` (index/slot bookkeeping, sift directions, grow paths),
|
|
367
|
+
`RingBuffer` (including the full-buffer `removeElementByIndex` shift), `Deque` apart from COLL-4
|
|
368
|
+
(expand, ring arithmetic, `#remove_internal_shift_backward` on a full deque all verified),
|
|
369
|
+
`HashMap` open addressing (Robin Hood probe, backward-shift delete, `rebuild` compaction, load-factor
|
|
370
|
+
accounting — the bins array is provably never saturated), `HashSet`, `ArraySet`, `Stack`,
|
|
371
|
+
`CuckooFilter` (alt-bucket reversibility confirmed), `union_find` (path halving), `Cache` LRU
|
|
372
|
+
list/weight accounting (the pre-`data.set` eviction window is unreachable), `CacheWTinylfu` +
|
|
373
|
+
`CacheRegionWTinylfu` + `wtinylfuDecisions`, `LoadingCache`, `Signal` add/remove/dispatch generation
|
|
374
|
+
tracking and once-handler removal, `SignalBinding`, `SimpleStateMachine(Description)`,
|
|
375
|
+
`FunctionCompiler`, `array_sort_quick` / `array_quick_sort_by_*`, `array_set_diff(_sorting)`,
|
|
376
|
+
`array_shuffle`, `binarySearchHighIndex`, and the remaining `array/` and `array/typed/` helpers.
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# Correctness review — `meep/src/core/math/`
|
|
2
|
+
|
|
3
|
+
Scope: 203 non-spec source files under `H:/git/moh/app/src/mir-engine/meep/src/core/math/`.
|
|
4
|
+
All findings below were reproduced by executing the actual modules under node 24 (ESM).
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
### MATH-1 Simplex noise truncates instead of floors the lattice cell index
|
|
9
|
+
- **Severity**: CRITICAL
|
|
10
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/math/noise/sdnoise.js:199`, `:278-279`, `:429-431`, `:686-689`
|
|
11
|
+
- **Defect**: All four simplex-noise variants derive the integer lattice cell with `x | 0` / `xs | 0`, which truncates **toward zero**. The reference implementation (Gustavson/McEwan `sdnoise1234.c`) uses `FASTFLOOR`. For any coordinate whose skewed value is negative and non-integral the wrong cell is selected, the corner offsets `x0/x1/...` leave their valid range, and the field becomes discontinuous (1D: unbounded). The sibling module `noise/create_simplex_noise_2d.js:88` already carries a comment explaining exactly this hazard and uses `Math.floor`; `sdnoise.js` was never fixed.
|
|
12
|
+
- **Failure scenario**:
|
|
13
|
+
- `sdnoise1(d, -0.9)` → **-22.0446** (`d[0] = 139.88`). Documented/reference range is `[-1, 1]`. In 1D the reference explicitly does *not* clamp `t1` because `|x1| <= 1` "always" — with truncation `x1 = -1.9`, `t1 = 1 - 3.61 = -2.61`, `t41 = 46.4`, and the corner contribution explodes. Sampling `x ∈ [-5, 5]` at 1e5 points gives a range of **[-323.81, +242.86]**; the same sweep over `[0, 5]` gives `[-0.173, 0.401]`.
|
|
14
|
+
- `sdnoise3(d, x, 0.3, 0.7)` swept over `x ∈ [-3, 3]` at 2e5 points has a maximum step between adjacent samples of **0.2335** (at x ≈ -1.75); the identical sweep over `x ∈ [1, 7]` has max step **0.00154**. Simplex noise is C¹-continuous, so a 150× jump is a discontinuity, not sampling resolution. Same for `sdnoise2` (0.355 vs 7.5e-5) and `sdnoise4` (0.722 vs 0.00106).
|
|
15
|
+
- Real caller: `engine/graphics/particles/particular/engine/simulator/SimulationStepCurlNoiseAcceleration.js:59` → `core/math/noise/curl_noise_3d.js:18` samples `sdnoise3` at `x - 231.141, ..., z - 54.4341`, i.e. permanently in the broken negative region for typical world coordinates.
|
|
16
|
+
- **Fix**: Replace every `x | 0` / `xs | 0` cell computation in `sdnoise.js` with `Math.floor(...)` (the existing `& 0xff` / `& 255` wraps already handle negative indices correctly). Note the pinned regression values in `sdnoise.spec.js:111,124,160,173` were captured from the buggy field and must be re-derived after the fix.
|
|
17
|
+
- **Confidence**: Confirmed
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
### MATH-2 `hammersley_sequence` seeds the radical-inverse iteration with `i/count`, producing duplicates
|
|
22
|
+
- **Severity**: CRITICAL
|
|
23
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/math/statistics/hammersley_sequence.js:10`
|
|
24
|
+
- **Defect**: The loop body is the correct incremental Halton "next value" step (Wikipedia *Halton sequence*, fast method: `x = d-n; y = d/b; while x<=y: y/=b; n = (b+1)y - x`), which requires the iteration to start from `t = 0` and be applied `i` times to yield the `i`-th radical inverse. The code instead initialises `t = i / count`, which is not a valid state of that recurrence, so the returned values are not a radical inverse and the set is not low-discrepancy — it contains repeats.
|
|
25
|
+
- **Failure scenario**: `hammersley_sequence(i, 2, 8)` for `i = 0..7` produces
|
|
26
|
+
`[0, 0.625, 0.125, 0.5625, 0.625, 0.3125, 0.5625, 0.4375]` — only **6 distinct values out of 8** (0.625 and 0.5625 each appear twice). The van der Corput / base-2 Halton sequence (what the sibling `halton_sequence(2, i)` correctly returns) is `[0, 0.5, 0.25, 0.75, 0.125, 0.625, 0.375, 0.875]`. Base 3 is equally wrong: produced `[0, 0.4583, 0.9167, 0.4861, 0.9444, 0.2176, 0.9722, 0.1466]` vs correct `[0, 1/3, 2/3, 1/9, 4/9, 7/9, 2/9, 5/9]`.
|
|
27
|
+
Two samples landing on the same value defeats the entire purpose of a low-discrepancy sequence (the doc line 2 says "Low discrepancy sequence").
|
|
28
|
+
- **Fix**: `let t = 0;` (the `count` parameter then becomes unused for this coordinate — the `i/count` term is the *first* Hammersley coordinate, which belongs in the caller alongside this radical inverse, exactly as `hammersley_sequence_2d.js:15-16` already does). Alternatively delegate to `halton_sequence(R, i)`, which is both correct and O(log i) rather than O(i).
|
|
29
|
+
- **Confidence**: Confirmed
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
### MATH-3 `computeStatisticalPercentile` selects the order statistic one rank too high
|
|
34
|
+
- **Severity**: MAJOR
|
|
35
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/math/statistics/computeStatisticalPercentile.js:23-24`
|
|
36
|
+
- **Defect**: `start = floor(n * low)` is a half-open lower bound while `end = ceil(n * high)` is used as an **inclusive** upper index. Both are one greater than the correct 0-based rank; only clamping to `n-1` hides it at `high = 1`. For a point query (`low === high === p`) the returned element is the one at index `floor(p*n)` instead of the `p`-th order statistic.
|
|
37
|
+
- **Failure scenario**: With `values = [1,2,3,4,5,6,7,8,9,10]`:
|
|
38
|
+
- `computeStatisticalPercentile(values, 0.9, 0.9)` → **10**. 10 is the *maximum* (100th percentile). Nearest-rank definition (Hyndman & Fan type 1): index `ceil(0.9*10) - 1 = 8` → **9**.
|
|
39
|
+
- `computeStatisticalPercentile(values, 0.5, 0.5)` → **6**, while `computeStatisticalMedian(values)` on the same data returns **5**. The library disagrees with itself about the 50th percentile.
|
|
40
|
+
- `computeStatisticalPercentile(values, 0, 0.4)` → **3**; the bottom 40 % of the data is `[1,2,3,4]`, whose (lower) median is **2** — the range wrongly includes index 4 (value 5, the 50th-percentile element).
|
|
41
|
+
- **Fix**: Use `ceil(n·p) - 1` for both bounds:
|
|
42
|
+
`const start = clamp(Math.ceil(n * percentile_low) - 1, 0, n - 1);`
|
|
43
|
+
`const end = clamp(Math.ceil(n * percentile_high) - 1, 0, n - 1);`
|
|
44
|
+
Verified this keeps both existing spec cases green (`(0, 0.5) → 2`, `(0.5, 1) → 7`) while fixing `(0.5, 0.5) → 5` and `(0.9, 0.9) → 9`.
|
|
45
|
+
- **Confidence**: Confirmed
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
### MATH-4 `roundFair` never rounds a negative number up
|
|
50
|
+
- **Severity**: MAJOR
|
|
51
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/math/random/roundFair.js:9`
|
|
52
|
+
- **Defect**: `number % 1` in JavaScript keeps the sign of the dividend, so for a negative input `mantissa` is negative. `random()` returns a value in `[0, 1)`, so `roll > mantissa` is unconditionally true and the function always takes the `Math.floor` branch. The documented contract — "a fraction is rounded up or down with probability equal to the fraction", i.e. an unbiased stochastic rounding with `E[roundFair(x)] === x` — is violated.
|
|
53
|
+
- **Failure scenario**: `roundFair(-2.3, Math.random)` returns **-3** for every draw (verified: `()=>0.9 → -3`, `()=>0.1 → -3`; mean over 10 000 `Math.random` draws = **-3.0**). Expected: -3 with probability 0.3 and -2 with probability 0.7, mean **-2.3**. Positive control `roundFair(2.3, Math.random)` gives mean 2.2967, so the positive path is correct.
|
|
54
|
+
- **Fix**: Use the Euclidean fraction: `const mantissa = number - Math.floor(number);` (or `euclidean_modulo(number, 1)`, which already exists in this package). With `-2.3` that gives `0.7`, and `roll > 0.7` (probability 0.3) → `floor = -3`, else → `ceil = -2`, mean -2.3.
|
|
55
|
+
- **Confidence**: Confirmed
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
### MATH-5 `fract` truncates toward zero and wraps at 2³¹
|
|
60
|
+
- **Severity**: MAJOR
|
|
61
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/math/fract.js:9`
|
|
62
|
+
- **Defect**: `const whole = v >> 0;` performs `ToInt32`, which (a) truncates toward zero rather than flooring and (b) wraps modulo 2³². The JSDoc explicitly cites the GLSL `fract` reference page and states the result "is calculated as x - floor(x)".
|
|
63
|
+
- **Failure scenario**:
|
|
64
|
+
- `fract(-0.1)` → **-0.1**; GLSL/documented `x - floor(x)` → **0.9**. `fract(-0.25)` → -0.25 vs 0.75.
|
|
65
|
+
- `fract(1e10)` → **8589934592**; expected **0**. (`1e10 >> 0 === 1410065408`.)
|
|
66
|
+
- `fract(2147483648.5)` → **4294967296.5**; expected **0.5**.
|
|
67
|
+
The 2³¹ wrap is wrong under *any* reading of the contract — the result is not even in `(-1, 1)`.
|
|
68
|
+
- **Fix**: `return v - Math.floor(v);`. Note `fract.spec.js:7-9` currently pins the truncating behaviour (`fract(-0.1) === -0.1`, `fract(-1.1) === -0.1`) and must be updated with the fix, not used as justification for the current code.
|
|
69
|
+
- **Confidence**: Confirmed
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
### MATH-6 `squirrel3` does not use 32-bit wrapping multiplication, so it is not the cited hash and collides
|
|
74
|
+
- **Severity**: MAJOR
|
|
75
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/math/hash/squirrel3.js:13`, `:18` (also `:15`, `:19`)
|
|
76
|
+
- **Defect**: The cited Squirrel3 (Eiserloh, GDC 2017) is defined over `unsigned int` with wrapping multiplies and *logical* right shifts. Here `value * BIT_NOISE1` and `hash *= BIT_NOISE3` are IEEE-754 double multiplies: an int32 operand times a ~2³² constant produces up to ~2⁶⁰, far beyond the 53-bit mantissa, so the low bits are **rounded away** before `ToInt32` takes them. Additionally `hash >> 8` (lines 15, 19) is an arithmetic shift where the reference uses a logical shift, so for negative intermediates different bits are mixed in.
|
|
77
|
+
- **Failure scenario**: `squirrel3(0)` → **436901382**; a faithful 32-bit implementation (`Math.imul` + `>>>`) gives **436901570**. `squirrel3(1)` → 1910996822 vs reference 725778245. More consequentially, the reference is a bijection on 32 bits: over the 200 000 consecutive inputs `0..199999` it produces **200 000 distinct outputs**, whereas `squirrel3` produces only **199 763** — 237 collisions purely from the lost mantissa bits. (This is almost certainly the cause of the "low entropy" note in the file's own header comment.)
|
|
78
|
+
- **Fix**: `let hash = Math.imul(value | 0, BIT_NOISE1);` … `hash = Math.imul(hash, BIT_NOISE3);`, and change both `>> 8` to `>>> 8`. Any callers persisting these hashes must be re-keyed.
|
|
79
|
+
- **Confidence**: Confirmed
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
### MATH-7 `m2_svd` / `m2_polar_decomp` emit factors in the opposite storage convention to their input
|
|
84
|
+
- **Severity**: MAJOR
|
|
85
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/math/linalg/m2/m2_svd.js:7-19`, `H:/git/moh/app/src/mir-engine/meep/src/core/math/linalg/m2/m2_polar_decomp.js:8`, `H:/git/moh/app/src/mir-engine/meep/src/core/math/linalg/m2/m2_polar_decomp_noS.js:5`
|
|
86
|
+
- **Defect**: The input `m` is read as transposed storage (`m2_polar_decomp_noS.js:20` computes `y = m[2] - m[1]`, i.e. `A01 - A10`, the negation of taichi's `A[1,0] - A[0,1]`), but `U`, `V` and `R` are written in plain row-major. Consequently the documented reconstruction identities do not hold when evaluated with this package's own `m2_multiply`/`m2_transpose`. The two polar docs already contradict each other (`m = S * R` vs `m = R * S`) and neither is the relation that actually holds.
|
|
87
|
+
- **Failure scenario**:
|
|
88
|
+
- `m = [1, -2, 5, 3]`. After `m2_svd(U, S, V, sig, m)`, computing `U·Σ·Vᵀ` exactly as `m2_svd.spec.js:16-20` does yields **`[1, 5, -2, 3]`** — the transpose of `m`, not `m`. The doc (`m2_svd.js:7`) asserts `m = U * Σ * V^T`. The existing spec only checks reconstruction for symmetric inputs (`[1,0,0,1]`, `[2,0,0,3]`, `[2,1e-7,1e-7,3]`), where a transpose is invisible; the one asymmetric case `[1,-2,5,3]` checks only singular-value ordering.
|
|
89
|
+
- `m = [1, 2, 3, 4]`. `m2_polar_decomp(R, S, m)` gives `R = [0.98058, 0.19612, -0.19612, 0.98058]`, `S = [0.58835, 2.15728, 2.15728, 4.51067]`. `m2_multiply(x, S, R)` (the documented `m = S * R`) yields **`[0.15385, 2.23077, 1.23077, 4.84615]`** — neither `m` nor `mᵀ`. The relation that actually holds is `m2_multiply(x, R, S) = [1, 3, 2, 4] = mᵀ`.
|
|
90
|
+
- Downstream consequence (outside this package but caused by it): `engine/physics/mls-mpm/MLS_MPM.js:373-374` stores `F` in transposed layout (`m2_multiply(F, p.F, m2_temp0)` is only correct under that convention) and then writes back `m2_multiply(m2_temp0, svd_u, sig); m2_multiply_transposed(p.F, m2_temp0, svd_v)`, which stores the untransposed `U Σ Vᵀ` into a transposed slot — i.e. the plastically-clamped deformation gradient is transposed on every substep whenever `F` is non-symmetric (any shear).
|
|
91
|
+
- **Fix**: Pick one convention and make it total. Cheapest correct change that preserves the taichi port: transpose the emitted rotations so all five arrays share the input's storage — write `R = [c, -s, s, c]` in `m2_polar_decomp_noS`, mirror it in the two `V` branches of `m2_svd.js:79-91`, and swap the operand order of `m2_multiply(U, U, V)` at `m2_svd.js:95`. Then add an asymmetric-input reconstruction case to `m2_svd.spec.js` (e.g. `[1,-2,5,3]`) so the invariant is pinned, and reconcile the three JSDoc blocks.
|
|
92
|
+
- **Confidence**: Confirmed
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
### MATH-8 `ParameterLookupTable.toBinaryBuffer` picks an item-count width one bit too small at exact powers of two
|
|
97
|
+
- **Severity**: MAJOR
|
|
98
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/math/lookup/ParameterLookupTable.js:110-122`
|
|
99
|
+
- **Defect**: The width is chosen from `Math.log2(itemCount)`, but the number of bits needed to store `itemCount` is `floor(log2(itemCount)) + 1`. At an exact power of two the test is off by one and too narrow a field is selected; `DataView.setUint8`/`setUint16` then silently truncate modulo the field.
|
|
100
|
+
- **Failure scenario**: A table with exactly **256** entries: `log2(256) = 8`, so `itemCountByteSize = 1` and `buffer.writeUint8(256)` writes **0**. `fromBinaryBuffer` then reads `itemCount = 0` and reconstructs an **empty** table — total silent data loss on round-trip. Same at 65 536 entries (`writeUint16(65536) → 0`). 255 and 257 entries round-trip correctly, so the failure is exactly at the boundaries.
|
|
101
|
+
- **Fix**: Compare against the value, not the log: `if (itemCount <= 0xFF) ... else if (itemCount <= 0xFFFF) ... else if (itemCount <= 0xFFFFFFFF) ...`.
|
|
102
|
+
- **Confidence**: Confirmed
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
### MATH-9 `copysign` flips the sign when the sign source is `+0`
|
|
107
|
+
- **Severity**: MINOR
|
|
108
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/math/copysign.js:9-14`
|
|
109
|
+
- **Defect**: The sign is decided by `(x > 0 && y > 0) || (x < 0 && y < 0)`. When `y` is `+0` (or `NaN`) neither disjunct holds, so the `else` branch negates `x`. C99 `copysign` takes the *sign bit* of `y`; the sign bit of `+0` is clear, so the result must be `+|x|`.
|
|
110
|
+
- **Failure scenario**: `copysign(3, 0)` → **-3**; C99/`tgmath.h` (which the JSDoc cites as the source) gives **+3**. `copysign(-3, 0)` → **+3** (correct by accident). `copysign(3, NaN)` → -3, where C gives +3. `copysign(3, -0)` → -3 (correct). The existing spec never passes `0` as `y`.
|
|
111
|
+
- **Fix**: `return (y < 0 || Object.is(y, -0)) ? -Math.abs(x) : Math.abs(x);`
|
|
112
|
+
- **Confidence**: Confirmed
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
### MATH-10 `softmax` overflows to `NaN` for large inputs
|
|
117
|
+
- **Severity**: MINOR
|
|
118
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/math/statistics/softmax.js:12`, `:19`
|
|
119
|
+
- **Defect**: `Math.exp` is applied to the raw components. `Math.exp(z) === Infinity` for `z > 709.78`, and `Infinity / Infinity === NaN`. The standard formulation (and every reference implementation, cf. the linked Wikipedia article's "numerical stability" note) subtracts `max(z)` first — mathematically identical because `softmax(z) === softmax(z - c)` for any constant `c`.
|
|
120
|
+
- **Failure scenario**: `softmax([1000, 1], 0, 2)` → **NaN**; the correct value is **1** (to within double precision). `softmax([800, 801], 1, 2)` → **NaN**; correct value **0.7311**.
|
|
121
|
+
- **Fix**: Compute `let max = -Infinity; for (j…) max = Math.max(max, z[j]);` then use `Math.exp(z[i] - max)` and `sum += Math.exp(z[j] - max)`.
|
|
122
|
+
- **Confidence**: Confirmed
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Checked and found correct (recorded so they are not re-reviewed)
|
|
127
|
+
|
|
128
|
+
- `linalg/lu_factor_linear_system.js` / `lu_solve_linear_system.js` — faithful port of VTK `LUFactorLinearSystem`/`LUSolveLinearSystem`; the column-major index map `A[j*size+i] ≡ A[i][j]` is applied consistently in every loop, including the row interchange and the `scale[maxI] = scale[j]` step.
|
|
129
|
+
- `idct_1d.js` / `idct_2d.js` — element-for-element match with the ffmpeg AAN `idct_1d`. `idct_2d` deliberately omits ffmpeg-exr's extra `*0.5`: the only caller is the JPEG decoder (`engine/asset/loaders/image/jpeg/idct8x8_float.js`), and the JPEG convention is exactly `1/2` per dimension. Verified DC round-trip: `F(0,0) = 8` → `f(x,y) = 1`.
|
|
130
|
+
- `linalg/solve_linear_system_GEPP_2x2.js` — exhaustively walked every zero-pattern branch (single, double, singular); column-swap/row-swap bookkeeping and the final un-swap of `x` are correct in all cases.
|
|
131
|
+
- `linalg/eigen/matrix_symmetric_3x3_eigen.js` — Numerical Recipes cyclic Jacobi; rotation formulas, third-index update, eigenvector accumulation and ascending sort all correct; `r = 3 - p - q` is valid for all three plane pairs.
|
|
132
|
+
- `linalg/givens/givens_rotation_coefficients.js` — both branches satisfy `-s·a + c·b = 0` and `c² + s² = 1`.
|
|
133
|
+
- `noise/create_simplex_noise_2d.js` — correct Fisher–Yates (`r = i + ~~(random()*(256-i))`, range `[i, 255]`), correct `Math.floor`, all `perm[]` indices bounded by 511.
|
|
134
|
+
- `spline/computeCatmullRomSpline.js`, `computeCatmullRomSplineUniformDistance.js`, `computeNonuniformCatmullRomSplineSample.js` — Wikipedia centripetal Catmull–Rom barycentric form; endpoint interpolation exact at `f = 0` and `f = 1`.
|
|
135
|
+
- `spline/interpolate_bicubic.js` — algebraically identical to the standard `p1 + 0.5t(p2-p0 + t(2p0-5p1+4p2-p3 + t(3(p1-p2)+p3-p0)))`.
|
|
136
|
+
- `solveCubic.js` — the trigonometric branch's `acos(3q/(pm))` argument is provably in `[-1,1]` whenever `ratio < 0`, so no clamp is needed.
|
|
137
|
+
- `physics/kinematics/computeInterceptPoint.js` — `(V·V - s²)t² + 2(V·D)t + D·D = 0` and the smallest-non-negative-root selection are both correct.
|
|
138
|
+
- `compute_legendre_polynomial.js` — exact match with Green's *Spherical Harmonic Lighting* reference.
|
|
139
|
+
- `modified_bessel_i0.js` — term recurrence `T_k = T_{k-1}·y/k²` matches `Σ (x²/4)^k/(k!)²`.
|
|
140
|
+
- `random/randomIntegerBetween.js`, `random/random_pick_weighted_index.js`, `random/seededRandom_Mulberry32.js`, `random/seededRandom_MersenneTwister.js` — inclusive/uniform bucketing, cumulative-weight search bounds, and both generator kernels verified. (Mulberry32's unbounded `_seed` accumulator was measured: after seeding at 2⁶⁰ the distinct-output count over 100 000 draws is 99 996 vs 99 999 at seed 0 — not a reportable degradation.)
|
|
141
|
+
- `statistics/computeStatisticalVariance.js` (population divisor `n`) and `statistics/computeStatisticalMedian.js` (lower median on even counts) are both **intentional**, pinned by their specs — not defects.
|
|
142
|
+
- `physics/brdf/*`, `physics/pdf/*`, `physics/irradiance/*` — `D_GGX` matches Filament's listing, `pdf_GGX` is self-consistent with it on the α parameterisation, `cone_cosine_from_roughness` is the exact inverse of `reflection_sample_weight`'s spherical gaussian, `pdf_normal` is the correct `N(0,σ)` density, and `ior_to_reflectivity`/`reflectivity_to_ior` are exact mutual inverses.
|
|
143
|
+
- `physics/mie/ri_*.js` — lookup positions are supplied in nanometres, so `ParameterLookupTable.sample(wavelength_nm, …)` interpolates on the right axis.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Unspecced files
|
|
148
|
+
|
|
149
|
+
67 source files under `core/math/` have no adjacent `*.spec.js`:
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
DEG_TO_RAD.js
|
|
153
|
+
EPSILON.js
|
|
154
|
+
FLT_EPSILON_16.js
|
|
155
|
+
FLT_EPSILON_32.js
|
|
156
|
+
FLT_EPSILON_64.js
|
|
157
|
+
GOLDEN_RATIO.js
|
|
158
|
+
NumericType.js
|
|
159
|
+
PI.js
|
|
160
|
+
PI2.js
|
|
161
|
+
PI_HALF.js
|
|
162
|
+
PI_RECIPROCAL.js
|
|
163
|
+
RAD_TO_DEG.js
|
|
164
|
+
build_gaussian_kernel_2d.js
|
|
165
|
+
euclidean_modulo.js
|
|
166
|
+
fabsf.js
|
|
167
|
+
hash/lowbias32.js
|
|
168
|
+
idct_1d.js
|
|
169
|
+
idct_2d.js
|
|
170
|
+
interval/intersects1D.js
|
|
171
|
+
interval/separation1D.js
|
|
172
|
+
linalg/lu_factor_linear_system.js
|
|
173
|
+
linalg/m2/m2_polar_decomp.js
|
|
174
|
+
linalg/m2/m2_polar_decomp_noS.js
|
|
175
|
+
linalg/sparse/spm_matvec.js
|
|
176
|
+
linalg/sparse/spm_matvec_transpose.js
|
|
177
|
+
lookup/ParameterLookupTable.js
|
|
178
|
+
lookup/ParameterLookupTableFlags.js
|
|
179
|
+
mix.js
|
|
180
|
+
modified_bessel_i0.js
|
|
181
|
+
noise/curl_noise_3d.js
|
|
182
|
+
noise/curl_noise_3dt.js
|
|
183
|
+
physics/brdf/diffuse_GGX.js
|
|
184
|
+
physics/brdf/fresnel_Schlick.js
|
|
185
|
+
physics/brdf/fresnel_Schlick_optimized.js
|
|
186
|
+
physics/brdf/fresnel_Schlick_original.js
|
|
187
|
+
physics/bsdf/bsdf_schlick.js
|
|
188
|
+
physics/ior_to_reflectivity.js
|
|
189
|
+
physics/mie/MIE_PARTICLES_STANDARD.js
|
|
190
|
+
physics/mie/MIE_PARTICLES_STANDARD_PRECOMPUTED.js
|
|
191
|
+
physics/mie/lorenz_mie_coefs.js
|
|
192
|
+
physics/mie/ri_air.js
|
|
193
|
+
physics/mie/ri_ammonium_sulfate.js
|
|
194
|
+
physics/mie/ri_brine.js
|
|
195
|
+
physics/mie/ri_dust.js
|
|
196
|
+
physics/mie/ri_pollen.js
|
|
197
|
+
physics/mie/ri_smoke.js
|
|
198
|
+
physics/mie/ri_soot.js
|
|
199
|
+
physics/mie/ri_water.js
|
|
200
|
+
physics/pdf/pdf_normal.js
|
|
201
|
+
physics/reflectivity_to_ior.js
|
|
202
|
+
random/generate_halton_jitter.js
|
|
203
|
+
random/randomBytes.js
|
|
204
|
+
random/random_pick_weighted_index.js
|
|
205
|
+
random/seededRandom.js
|
|
206
|
+
spline/catmull_rom_compute_T.js
|
|
207
|
+
spline/computeNonuniformCaltmullRomSplineDerivative.js
|
|
208
|
+
spline/computeNonuniformCatmullRomSplineSample.js
|
|
209
|
+
spline/interpolate_bicubic.js
|
|
210
|
+
spline/spline3_hermite_intersection_spline3_hermite_2d.js
|
|
211
|
+
spline/spline3_hermite_intersects_spline3_hermite.js
|
|
212
|
+
spline/v3_computeCatmullRomSplineUniformDistance.js
|
|
213
|
+
sqr.js
|
|
214
|
+
statistics/computeStatisticalMedian.js
|
|
215
|
+
statistics/computeStatisticalStandardDeviation.js
|
|
216
|
+
statistics/generate_hammersley_jitter.js
|
|
217
|
+
statistics/hammersley_sequence.js
|
|
218
|
+
statistics/hammersley_sequence_2d.js
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Of the 10 findings, 4 land in unspecced files (`hammersley_sequence.js`, `ParameterLookupTable.js`, `m2_polar_decomp*.js`); the other 6 land in files whose specs exist but only exercise the working half of the input domain (`sdnoise.spec.js` never samples negative coordinates for 1/3/4D, `fract.spec.js` pins the bug, `copysign.spec.js` never passes `y = 0`, `m2_svd.spec.js` reconstructs only symmetric inputs, `roundFair.spec.js` never passes a negative, `computeStatisticalPercentile.spec.js` never makes a point query).
|