@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.
Files changed (295) hide show
  1. package/README.md +2 -2
  2. package/build/bundle-worker-image-decoder.js +1 -1
  3. package/package.json +1 -1
  4. package/src/REVIEW_2026_08_06/adv-core.md +511 -0
  5. package/src/REVIEW_2026_08_06/adv-gfx.md +480 -0
  6. package/src/REVIEW_2026_08_06/adv-view.md +446 -0
  7. package/src/REVIEW_2026_08_06/api-core.md +296 -0
  8. package/src/REVIEW_2026_08_06/api-engine.md +195 -0
  9. package/src/REVIEW_2026_08_06/bvh-phys-sound-net.md +507 -0
  10. package/src/REVIEW_2026_08_06/core-binary.md +348 -0
  11. package/src/REVIEW_2026_08_06/core-collection.md +376 -0
  12. package/src/REVIEW_2026_08_06/core-math.md +221 -0
  13. package/src/REVIEW_2026_08_06/core-process.md +266 -0
  14. package/src/REVIEW_2026_08_06/core-science.md +223 -0
  15. package/src/REVIEW_2026_08_06/engine-ai-nav.md +279 -0
  16. package/src/REVIEW_2026_08_06/engine-animation.md +167 -0
  17. package/src/REVIEW_2026_08_06/engine-asset.md +166 -0
  18. package/src/REVIEW_2026_08_06/engine-ecs.md +920 -0
  19. package/src/REVIEW_2026_08_06/engine-input.md +197 -0
  20. package/src/REVIEW_2026_08_06/engine-misc.md +248 -0
  21. package/src/REVIEW_2026_08_06/engine-terrain.md +52 -0
  22. package/src/REVIEW_2026_08_06/generation.md +176 -0
  23. package/src/REVIEW_2026_08_06/geom-2d.md +434 -0
  24. package/src/REVIEW_2026_08_06/geom-3d.md +117 -0
  25. package/src/REVIEW_2026_08_06/gfx-gi.md +143 -0
  26. package/src/REVIEW_2026_08_06/gfx-render.md +740 -0
  27. package/src/REVIEW_2026_08_06/gfx-texture.md +690 -0
  28. package/src/REVIEW_2026_08_06/prior-criticals.md +49 -0
  29. package/src/REVIEW_2026_08_06/prior-highs.md +138 -0
  30. package/src/REVIEW_2026_08_06/test-gaps.md +364 -0
  31. package/src/REVIEW_2026_08_06/view.md +667 -0
  32. package/src/REVIEW_2026_08_06.md +571 -0
  33. package/src/core/binary/BinaryBuffer.d.ts.map +1 -1
  34. package/src/core/binary/BinaryBuffer.js +12 -1
  35. package/src/core/binary/BitImage2.d.ts +5 -1
  36. package/src/core/binary/BitImage2.d.ts.map +1 -1
  37. package/src/core/binary/BitImage2.js +5 -1
  38. package/src/core/bvh8/build/NodeProxy.d.ts.map +1 -1
  39. package/src/core/bvh8/build/NodeProxy.js +314 -308
  40. package/src/core/collection/list/List.d.ts +15 -0
  41. package/src/core/collection/list/List.d.ts.map +1 -1
  42. package/src/core/collection/list/List.js +34 -0
  43. package/src/core/collection/list/SortedListProjection.d.ts.map +1 -1
  44. package/src/core/collection/list/SortedListProjection.js +4 -1
  45. package/src/core/collection/map/BiMap.d.ts +25 -2
  46. package/src/core/collection/map/BiMap.d.ts.map +1 -1
  47. package/src/core/collection/map/BiMap.js +57 -3
  48. package/src/core/debug/matchers/IsAnything.d.ts.map +1 -1
  49. package/src/core/debug/matchers/IsAnything.js +5 -1
  50. package/src/core/events/signal/Signal.d.ts.map +1 -1
  51. package/src/core/events/signal/Signal.js +3 -1
  52. package/src/core/geom/2d/polygon/TRIANGULATION_DESIGN.md +433 -0
  53. package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.d.ts +31 -0
  54. package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.d.ts.map +1 -0
  55. package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.js +84 -0
  56. package/src/core/geom/2d/polygon/polygon2_signed_area.d.ts +24 -2
  57. package/src/core/geom/2d/polygon/polygon2_signed_area.d.ts.map +1 -1
  58. package/src/core/geom/2d/polygon/polygon2_signed_area.js +52 -34
  59. package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.d.ts +28 -0
  60. package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.d.ts.map +1 -0
  61. package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.js +86 -0
  62. package/src/core/geom/2d/polygon/polygon2_triangulate.d.ts +40 -0
  63. package/src/core/geom/2d/polygon/polygon2_triangulate.d.ts.map +1 -0
  64. package/src/core/geom/2d/polygon/polygon2_triangulate.js +1280 -0
  65. package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.d.ts +28 -0
  66. package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.d.ts.map +1 -0
  67. package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.js +77 -0
  68. package/src/core/geom/2d/triangle/tri2_rasterize_conservative.d.ts +6 -3
  69. package/src/core/geom/2d/triangle/tri2_rasterize_conservative.d.ts.map +1 -1
  70. package/src/core/geom/2d/triangle/tri2_rasterize_conservative.js +26 -13
  71. package/src/core/geom/2d/triangle/tri2_signed_area.d.ts +17 -1
  72. package/src/core/geom/2d/triangle/tri2_signed_area.d.ts.map +1 -1
  73. package/src/core/geom/2d/triangle/tri2_signed_area.js +35 -23
  74. package/src/core/geom/2d/v2_morton_encode.d.ts +20 -0
  75. package/src/core/geom/2d/v2_morton_encode.d.ts.map +1 -0
  76. package/src/core/geom/2d/v2_morton_encode.js +23 -0
  77. package/src/core/geom/3d/atlas/segment/atlas_merge_charts.d.ts.map +1 -1
  78. package/src/core/geom/3d/atlas/segment/atlas_merge_charts.js +4 -1
  79. package/src/core/geom/3d/polygon/polygon3_compute_normal.d.ts +27 -0
  80. package/src/core/geom/3d/polygon/polygon3_compute_normal.d.ts.map +1 -0
  81. package/src/core/geom/3d/polygon/polygon3_compute_normal.js +60 -0
  82. package/src/core/geom/3d/polygon/polygon3_triangulate.d.ts +34 -0
  83. package/src/core/geom/3d/polygon/polygon3_triangulate.d.ts.map +1 -0
  84. package/src/core/geom/3d/polygon/polygon3_triangulate.js +94 -0
  85. package/src/core/geom/3d/quaternion/quat3_multiply.d.ts +8 -2
  86. package/src/core/geom/3d/quaternion/quat3_multiply.d.ts.map +1 -1
  87. package/src/core/geom/3d/quaternion/quat3_multiply.js +8 -2
  88. package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts.map +1 -1
  89. package/src/core/geom/3d/shape/ConvexHullShape3D.js +32 -2
  90. package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.d.ts +23 -6
  91. package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.d.ts.map +1 -1
  92. package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.js +268 -124
  93. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.d.ts +17 -0
  94. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.d.ts.map +1 -0
  95. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.js +44 -0
  96. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.d.ts +20 -0
  97. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.d.ts.map +1 -0
  98. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.js +40 -0
  99. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_closed.d.ts +19 -0
  100. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_closed.d.ts.map +1 -0
  101. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_closed.js +34 -0
  102. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_manifold.d.ts +22 -0
  103. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_manifold.d.ts.map +1 -0
  104. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_manifold.js +37 -0
  105. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.d.ts +87 -0
  106. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.d.ts.map +1 -0
  107. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.js +599 -0
  108. package/src/core/geom/3d/triangle/tri3_compute_signed_volume.d.ts +32 -0
  109. package/src/core/geom/3d/triangle/tri3_compute_signed_volume.d.ts.map +1 -1
  110. package/src/core/geom/3d/triangle/tri3_compute_signed_volume.js +66 -7
  111. package/src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.d.ts +22 -1
  112. package/src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.d.ts.map +1 -1
  113. package/src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.js +83 -38
  114. package/src/core/geom/MANIFOLD_COMPARISON_2026_08_01.md +554 -0
  115. package/src/core/geom/vec3/v3_cotangent.d.ts +6 -2
  116. package/src/core/geom/vec3/v3_cotangent.d.ts.map +1 -1
  117. package/src/core/geom/vec3/v3_cotangent.js +6 -2
  118. package/src/core/geom/vec3/v3_cross.d.ts +6 -2
  119. package/src/core/geom/vec3/v3_cross.d.ts.map +1 -1
  120. package/src/core/geom/vec3/v3_cross.js +6 -2
  121. package/src/core/geom/vec3/v3_multiply.d.ts +6 -2
  122. package/src/core/geom/vec3/v3_multiply.d.ts.map +1 -1
  123. package/src/core/geom/vec3/v3_multiply.js +6 -2
  124. package/src/core/geom/vec3/v3_subtract.d.ts +6 -2
  125. package/src/core/geom/vec3/v3_subtract.d.ts.map +1 -1
  126. package/src/core/geom/vec3/v3_subtract.js +6 -2
  127. package/src/core/localization/Localization.js +1 -1
  128. package/src/core/math/random/randomSeed.d.ts +13 -0
  129. package/src/core/math/random/randomSeed.d.ts.map +1 -0
  130. package/src/core/math/random/randomSeed.js +16 -0
  131. package/src/core/math/spline/spline3_hermite_apply_transform.d.ts +50 -0
  132. package/src/core/math/spline/spline3_hermite_apply_transform.d.ts.map +1 -0
  133. package/src/core/math/spline/spline3_hermite_apply_transform.js +107 -0
  134. package/src/core/math/spline/spline3_hermite_reverse.d.ts +34 -0
  135. package/src/core/math/spline/spline3_hermite_reverse.d.ts.map +1 -0
  136. package/src/core/math/spline/spline3_hermite_reverse.js +49 -0
  137. package/src/core/math/statistics/hammersley_sequence.d.ts +17 -4
  138. package/src/core/math/statistics/hammersley_sequence.d.ts.map +1 -1
  139. package/src/core/math/statistics/hammersley_sequence.js +25 -16
  140. package/src/core/process/Future.d.ts.map +1 -1
  141. package/src/core/process/Future.js +12 -0
  142. package/src/core/process/PromiseWatcher.d.ts.map +1 -1
  143. package/src/core/process/PromiseWatcher.js +9 -2
  144. package/src/core/process/executor/ConcurrentExecutor.d.ts.map +1 -1
  145. package/src/core/process/executor/ConcurrentExecutor.js +12 -2
  146. package/src/core/process/worker/WorkerProxy.d.ts.map +1 -1
  147. package/src/core/process/worker/WorkerProxy.js +5 -2
  148. package/src/core/process/worker/extractTransferables.d.ts.map +1 -1
  149. package/src/core/process/worker/extractTransferables.js +4 -1
  150. package/src/engine/Engine.d.ts +12 -12
  151. package/src/engine/Engine.d.ts.map +1 -1
  152. package/src/engine/Engine.js +149 -10
  153. package/src/engine/EngineHarness.d.ts.map +1 -1
  154. package/src/engine/EngineHarness.js +4 -1
  155. package/src/engine/asset/loaders/ArrayBufferLoader.d.ts.map +1 -1
  156. package/src/engine/asset/loaders/ArrayBufferLoader.js +7 -0
  157. package/src/engine/ecs/speaker/lines/sets/LineSetDescription.d.ts.map +1 -1
  158. package/src/engine/ecs/speaker/lines/sets/LineSetDescription.js +3 -1
  159. package/src/engine/ecs/terrain/ecs/TerrainSystem.d.ts.map +1 -1
  160. package/src/engine/ecs/terrain/ecs/TerrainSystem.js +4 -1
  161. package/src/engine/ecs/terrain/tiles/TerrainTileManager.d.ts +29 -0
  162. package/src/engine/ecs/terrain/tiles/TerrainTileManager.d.ts.map +1 -1
  163. package/src/engine/ecs/terrain/tiles/TerrainTileManager.js +84 -23
  164. package/src/engine/graphics/CONTEXT_LOSS_RECOVERY_PLAN.md +446 -0
  165. package/src/engine/graphics/GraphicsEngine.d.ts +56 -7
  166. package/src/engine/graphics/GraphicsEngine.d.ts.map +1 -1
  167. package/src/engine/graphics/GraphicsEngine.js +196 -24
  168. package/src/engine/graphics/context/WebGLContextFailureReason.d.ts +9 -0
  169. package/src/engine/graphics/context/WebGLContextFailureReason.d.ts.map +1 -0
  170. package/src/engine/graphics/context/WebGLContextFailureReason.js +17 -0
  171. package/src/engine/graphics/context/WebGLContextMonitor.d.ts +106 -0
  172. package/src/engine/graphics/context/WebGLContextMonitor.d.ts.map +1 -0
  173. package/src/engine/graphics/context/WebGLContextMonitor.js +341 -0
  174. package/src/engine/graphics/context/WebGLContextState.d.ts +14 -0
  175. package/src/engine/graphics/context/WebGLContextState.d.ts.map +1 -0
  176. package/src/engine/graphics/context/WebGLContextState.js +24 -0
  177. package/src/engine/graphics/context/testWebGLContextLoss.d.ts +2 -0
  178. package/src/engine/graphics/context/testWebGLContextLoss.d.ts.map +1 -0
  179. package/src/engine/graphics/context/testWebGLContextLoss.js +696 -0
  180. package/src/engine/graphics/ecs/camera/serialization/CameraSerializationAdapter.d.ts.map +1 -1
  181. package/src/engine/graphics/ecs/camera/serialization/CameraSerializationAdapter.js +68 -32
  182. package/src/engine/graphics/ecs/camera/serialization/CameraSerializationUpgrader_0_1.d.ts +14 -0
  183. package/src/engine/graphics/ecs/camera/serialization/CameraSerializationUpgrader_0_1.d.ts.map +1 -0
  184. package/src/engine/graphics/ecs/camera/serialization/CameraSerializationUpgrader_0_1.js +41 -0
  185. package/src/engine/graphics/ecs/decal/v2/FPDecalSystem.js +1 -1
  186. package/src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.d.ts.map +1 -1
  187. package/src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.js +5 -0
  188. package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts +5 -1
  189. package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts.map +1 -1
  190. package/src/engine/graphics/ecs/trail2d/Trail2D.js +15 -0
  191. package/src/engine/graphics/ecs/trail2d/Trail2DSystem.d.ts +1 -1
  192. package/src/engine/graphics/ecs/trail2d/Trail2DSystem.d.ts.map +1 -1
  193. package/src/engine/graphics/ecs/trail2d/Trail2DSystem.js +2 -1
  194. package/src/engine/graphics/ecs/trail3d/Trail3D.d.ts +4 -1
  195. package/src/engine/graphics/ecs/trail3d/Trail3D.d.ts.map +1 -1
  196. package/src/engine/graphics/ecs/trail3d/Trail3D.js +488 -476
  197. package/src/engine/graphics/ecs/trail3d/Trail3DSystem.d.ts +1 -1
  198. package/src/engine/graphics/ecs/trail3d/Trail3DSystem.d.ts.map +1 -1
  199. package/src/engine/graphics/ecs/trail3d/Trail3DSystem.js +280 -253
  200. package/src/engine/graphics/geometry/MikkT/BuildNeighborsFast.d.ts.map +1 -1
  201. package/src/engine/graphics/geometry/MikkT/BuildNeighborsFast.js +3 -2
  202. package/src/engine/graphics/geometry/MikkT/GenerateSharedVerticesIndexList.d.ts.map +1 -1
  203. package/src/engine/graphics/geometry/MikkT/GenerateSharedVerticesIndexList.js +3 -1
  204. package/src/engine/graphics/geometry/computeMeshSurfaceArea.js +1 -1
  205. package/src/engine/graphics/load_and_set_cubemap_v0.d.ts +5 -0
  206. package/src/engine/graphics/load_and_set_cubemap_v0.d.ts.map +1 -1
  207. package/src/engine/graphics/load_and_set_cubemap_v0.js +26 -21
  208. package/src/engine/graphics/particles/particular/group/ParticleGroup.d.ts.map +1 -1
  209. package/src/engine/graphics/particles/particular/group/ParticleGroup.js +19 -8
  210. package/src/engine/graphics/particles/particular/group/optimizeCommandQueue.js +1 -1
  211. package/src/engine/graphics/texture/atlas/AtlasLookupTexture.d.ts.map +1 -1
  212. package/src/engine/graphics/texture/atlas/AtlasLookupTexture.js +10 -3
  213. package/src/engine/graphics/texture/atlas/TextureAtlas.d.ts.map +1 -1
  214. package/src/engine/graphics/texture/atlas/TextureAtlas.js +4 -1
  215. package/src/engine/graphics/texture/atlas/gpu/WebGLTextureAtlas.d.ts.map +1 -1
  216. package/src/engine/graphics/texture/atlas/gpu/WebGLTextureAtlas.js +11 -0
  217. package/src/engine/graphics/texture/cubemap/load_environment_map.d.ts.map +1 -1
  218. package/src/engine/graphics/texture/cubemap/load_environment_map.js +5 -2
  219. package/src/engine/graphics/texture/sampler/distance/computeSignedDistanceField_Chamfer.d.ts.map +1 -1
  220. package/src/engine/graphics/texture/sampler/distance/computeSignedDistanceField_Chamfer.js +26 -18
  221. package/src/engine/graphics/texture/sampler/filter/sampler2d_blur_gaussian.d.ts +15 -2
  222. package/src/engine/graphics/texture/sampler/filter/sampler2d_blur_gaussian.d.ts.map +1 -1
  223. package/src/engine/graphics/texture/sampler/filter/sampler2d_blur_gaussian.js +29 -3
  224. package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.d.ts +11 -1
  225. package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.d.ts.map +1 -1
  226. package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.js +158 -171
  227. package/src/engine/graphics/texture/sampler/sampler2d_paint.js +1 -1
  228. package/src/engine/graphics/texture/sampler/util/bitSet2Sampler2D.js +2 -2
  229. package/src/engine/input/ecs/util/TopDownCameraControllerHelper.d.ts.map +1 -1
  230. package/src/engine/input/ecs/util/TopDownCameraControllerHelper.js +10 -3
  231. package/src/engine/navigation/ecs/components/PathSerializationUpgrader_2_3.d.ts +14 -0
  232. package/src/engine/navigation/ecs/components/PathSerializationUpgrader_2_3.d.ts.map +1 -0
  233. package/src/engine/navigation/ecs/components/PathSerializationUpgrader_2_3.js +38 -0
  234. package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts.map +1 -1
  235. package/src/engine/network/orchestrator/ServerAuthoritativeClient.js +433 -425
  236. package/src/engine/physics/constraint/solve_constraints.d.ts +4 -1
  237. package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -1
  238. package/src/engine/physics/constraint/solve_constraints.js +38 -13
  239. package/src/engine/physics/contact/ManifoldStore.d.ts +15 -3
  240. package/src/engine/physics/contact/ManifoldStore.d.ts.map +1 -1
  241. package/src/engine/physics/contact/ManifoldStore.js +15 -3
  242. package/src/engine/physics/ecs/Joint.d.ts +7 -4
  243. package/src/engine/physics/ecs/Joint.d.ts.map +1 -1
  244. package/src/engine/physics/ecs/Joint.js +7 -4
  245. package/src/engine/physics/ecs/PhysicsSystem.d.ts +14 -0
  246. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  247. package/src/engine/physics/ecs/PhysicsSystem.js +16 -0
  248. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  249. package/src/engine/physics/fluid/ecs/FluidSystem.d.ts +3 -3
  250. package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts +15 -7
  251. package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts.map +1 -1
  252. package/src/engine/physics/narrowphase/convex_convex_manifold.js +33 -10
  253. package/src/engine/physics/narrowphase/refine_ray_concave.d.ts +6 -2
  254. package/src/engine/physics/narrowphase/refine_ray_concave.d.ts.map +1 -1
  255. package/src/engine/physics/narrowphase/refine_ray_concave.js +6 -2
  256. package/src/engine/physics/narrowphase/refine_ray_hit.d.ts +6 -2
  257. package/src/engine/physics/narrowphase/refine_ray_hit.d.ts.map +1 -1
  258. package/src/engine/physics/narrowphase/refine_ray_hit.js +6 -2
  259. package/src/engine/physics/queries/raycast.d.ts.map +1 -1
  260. package/src/engine/physics/queries/raycast.js +11 -4
  261. package/src/engine/save/storage/IndexedDBStorage.d.ts.map +1 -1
  262. package/src/engine/save/storage/IndexedDBStorage.js +21 -3
  263. package/src/engine/simulation/Ticker.d.ts +12 -0
  264. package/src/engine/simulation/Ticker.d.ts.map +1 -1
  265. package/src/engine/simulation/Ticker.js +18 -0
  266. package/src/engine/sound/simulation/AcousticSimulator.d.ts.map +1 -1
  267. package/src/engine/sound/simulation/AcousticSimulator.js +9 -1
  268. package/src/engine/sound/simulation/core/VolumeField.d.ts +6 -2
  269. package/src/engine/sound/simulation/core/VolumeField.d.ts.map +1 -1
  270. package/src/engine/sound/simulation/core/VolumeField.js +6 -2
  271. package/src/engine/sound/simulation/probe/AcousticProbeField.d.ts +6 -2
  272. package/src/engine/sound/simulation/probe/AcousticProbeField.d.ts.map +1 -1
  273. package/src/engine/sound/simulation/probe/AcousticProbeField.js +6 -2
  274. package/src/engine/sound/simulation/probe/acoustic_probe_transfer.d.ts +6 -2
  275. package/src/engine/sound/simulation/probe/acoustic_probe_transfer.d.ts.map +1 -1
  276. package/src/engine/sound/simulation/probe/acoustic_probe_transfer.js +6 -2
  277. package/src/engine/sound/simulation/probe/bakeProbeReflectors.d.ts +3 -1
  278. package/src/engine/sound/simulation/probe/bakeProbeReflectors.d.ts.map +1 -1
  279. package/src/engine/sound/simulation/probe/bakeProbeReflectors.js +3 -1
  280. package/src/engine/ui/GUIEngine.d.ts.map +1 -1
  281. package/src/engine/ui/GUIEngine.js +8 -1
  282. package/src/generation/theme/TerrainTheme.d.ts.map +1 -1
  283. package/src/generation/theme/TerrainTheme.js +2 -1
  284. package/src/generation/theme/ThemeEngine.d.ts.map +1 -1
  285. package/src/generation/theme/ThemeEngine.js +6 -5
  286. package/src/view/ViewGroup.d.ts.map +1 -1
  287. package/src/view/ViewGroup.js +9 -7
  288. package/src/view/graphics/WebGLContextFailureView.d.ts +19 -0
  289. package/src/view/graphics/WebGLContextFailureView.d.ts.map +1 -0
  290. package/src/view/graphics/WebGLContextFailureView.js +76 -0
  291. package/src/view/minimap/Minimap.d.ts +8 -0
  292. package/src/view/minimap/Minimap.d.ts.map +1 -1
  293. package/src/view/minimap/Minimap.js +16 -3
  294. package/src/view/minimap/dom/MinimapMarkerView.d.ts.map +1 -1
  295. 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).