devagent-physical-engine 0.12.2__tar.gz → 0.12.4__tar.gz

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 (154) hide show
  1. {devagent_physical_engine-0.12.2/src/devagent_physical_engine.egg-info → devagent_physical_engine-0.12.4}/PKG-INFO +6 -2
  2. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/README.md +5 -1
  3. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/pyproject.toml +1 -1
  4. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/__init__.py +1 -1
  5. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/ros2/moveit_scene.py +114 -6
  6. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/ros2/scene_probe.py +94 -24
  7. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/ros2/tool_scene.py +62 -14
  8. devagent_physical_engine-0.12.4/src/devagent_physical_engine/ros2/tool_scene_probe.py +365 -0
  9. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4/src/devagent_physical_engine.egg-info}/PKG-INFO +6 -2
  10. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine.egg-info/SOURCES.txt +2 -0
  11. devagent_physical_engine-0.12.4/tests/test_moveit_scene_v0123.py +213 -0
  12. devagent_physical_engine-0.12.4/tests/test_tool_scene_v0124.py +122 -0
  13. devagent_physical_engine-0.12.2/src/devagent_physical_engine/ros2/tool_scene_probe.py +0 -201
  14. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/NOTICE +0 -0
  15. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/setup.cfg +0 -0
  16. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/agent/__init__.py +0 -0
  17. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/agent/compiler.py +0 -0
  18. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/agent/contracts.py +0 -0
  19. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/agent/coordinator.py +0 -0
  20. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/agent/critic.py +0 -0
  21. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/agent/evidence.py +0 -0
  22. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/agent/interpreter.py +0 -0
  23. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/agent/planner.py +0 -0
  24. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/agent/recovery.py +0 -0
  25. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/agent/routing.py +0 -0
  26. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/agent/runtime.py +0 -0
  27. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/agent/semantic.py +0 -0
  28. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/agent/structured.py +0 -0
  29. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/ai_cli.py +0 -0
  30. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/cli.py +0 -0
  31. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/continuous_clearance.py +0 -0
  32. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/doctor.py +0 -0
  33. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/engineering_agent.py +0 -0
  34. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/engineering_request.py +0 -0
  35. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/execution.py +0 -0
  36. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/models.py +0 -0
  37. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/operating_envelope.py +0 -0
  38. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/optimization/__init__.py +0 -0
  39. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/optimization/benchmark.py +0 -0
  40. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/optimization/candidates.py +0 -0
  41. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/optimization/contracts.py +0 -0
  42. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/optimization/evaluator.py +0 -0
  43. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/optimization/evidence.py +0 -0
  44. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/optimization/experience.py +0 -0
  45. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/optimization/measured.py +0 -0
  46. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/optimization/optimizer.py +0 -0
  47. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/optimization/orchestrator.py +0 -0
  48. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/physical_campaign.py +0 -0
  49. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/physical_evidence.py +0 -0
  50. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/physical_motion.py +0 -0
  51. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/planning.py +0 -0
  52. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/preexecution_contract.py +0 -0
  53. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/provider_adapters/__init__.py +0 -0
  54. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/provider_adapters/anthropic.py +0 -0
  55. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/provider_adapters/common.py +0 -0
  56. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/provider_adapters/factory.py +0 -0
  57. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/provider_adapters/gemini.py +0 -0
  58. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/provider_adapters/openai.py +0 -0
  59. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/provider_qualification.py +0 -0
  60. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/providers.py +0 -0
  61. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/qualification.py +0 -0
  62. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/qualification_cli.py +0 -0
  63. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/qualification_harness.py +0 -0
  64. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/reference_workcell.py +0 -0
  65. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/robot_platform.py +0 -0
  66. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/robots.py +0 -0
  67. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/ros2/__init__.py +0 -0
  68. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/ros2/acceptance.py +0 -0
  69. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/ros2/commands.py +0 -0
  70. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/ros2/continuous_clearance_probe.py +0 -0
  71. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/ros2/doctor.py +0 -0
  72. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/ros2/fk_pose.py +0 -0
  73. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/ros2/fk_probe.py +0 -0
  74. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/ros2/frame_alignment.py +0 -0
  75. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/ros2/gazebo_cli.py +0 -0
  76. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/ros2/gazebo_pose.py +0 -0
  77. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/ros2/gazebo_world.py +0 -0
  78. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/ros2/joint_state_recorder.py +0 -0
  79. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/ros2/measured_motion.py +0 -0
  80. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/ros2/moveit_joint_plan_probe.py +0 -0
  81. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/ros2/moveit_task_planner.py +0 -0
  82. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/ros2/preexecution.py +0 -0
  83. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/ros2/qualification.py +0 -0
  84. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/ros2/qualification_v10.py +0 -0
  85. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/ros2/qualification_v11.py +0 -0
  86. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/ros2/qualification_v12.py +0 -0
  87. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/ros2/state_validity_probe.py +0 -0
  88. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/ros2/task_adapter.py +0 -0
  89. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/ros2/task_visualization.py +0 -0
  90. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/ros2/tf_probe.py +0 -0
  91. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/ros2/trajectory.py +0 -0
  92. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/ros2/ur5e.py +0 -0
  93. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/ros2/ur5e_adapter.py +0 -0
  94. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/ros2/ur5e_v10_adapter.py +0 -0
  95. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/ros2/workpiece_follow.py +0 -0
  96. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/ros2/workpiece_gazebo_follower.py +0 -0
  97. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/ros2/workpiece_scene.py +0 -0
  98. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/ros2/workpiece_scene_probe.py +0 -0
  99. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/setup_profile.py +0 -0
  100. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/sim_real_qualification.py +0 -0
  101. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/simulation.py +0 -0
  102. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/simulation_platform.py +0 -0
  103. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/tool_collision.py +0 -0
  104. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/trajectory_qualification.py +0 -0
  105. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/twin.py +0 -0
  106. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/twin_builder.py +0 -0
  107. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/twin_materialization.py +0 -0
  108. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/verification.py +0 -0
  109. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine/workpiece_lifecycle.py +0 -0
  110. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine.egg-info/dependency_links.txt +0 -0
  111. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine.egg-info/entry_points.txt +0 -0
  112. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine.egg-info/requires.txt +0 -0
  113. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/src/devagent_physical_engine.egg-info/top_level.txt +0 -0
  114. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_agent_core.py +0 -0
  115. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_agent_semantic_hardening.py +0 -0
  116. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_ai_cli.py +0 -0
  117. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_ai_cli_v07.py +0 -0
  118. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_continuous_clearance_v011.py +0 -0
  119. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_core.py +0 -0
  120. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_engineering_request_v07.py +0 -0
  121. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_gazebo_cli_v0122.py +0 -0
  122. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_interpreter_profiles_v08.py +0 -0
  123. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_live_provider_qualification.py +0 -0
  124. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_measured_motion_v10.py +0 -0
  125. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_natural_language_agent_v07.py +0 -0
  126. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_openai_strict_schema_v0113.py +0 -0
  127. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_optimization_v05.py +0 -0
  128. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_physical_campaign_v09.py +0 -0
  129. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_physical_evidence_v09.py +0 -0
  130. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_physical_motion_v09.py +0 -0
  131. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_planning_fallback_v012.py +0 -0
  132. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_preexecution_contract_v10.py +0 -0
  133. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_provider_and_qualification.py +0 -0
  134. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_qualification_cli_lifecycle_v06.py +0 -0
  135. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_qualification_harness_v06.py +0 -0
  136. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_readme_pypi.py +0 -0
  137. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_real_provider_adapters.py +0 -0
  138. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_reference_workcell_v012.py +0 -0
  139. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_robot_platform_v08.py +0 -0
  140. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_ros2_production_receipts_v011.py +0 -0
  141. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_ros2_simulation.py +0 -0
  142. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_ros_trajectory_v09.py +0 -0
  143. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_setup_profile.py +0 -0
  144. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_sim_real_qualification_v011.py +0 -0
  145. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_simulation_platform_v08.py +0 -0
  146. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_tool_collision_v011.py +0 -0
  147. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_trajectory_qualification_v09.py +0 -0
  148. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_twin_materialization_v10.py +0 -0
  149. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_twin_v08.py +0 -0
  150. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_ur5e_adapter_scopes_v09.py +0 -0
  151. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_ur5e_v10_adapter_scopes.py +0 -0
  152. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_v052_stabilization.py +0 -0
  153. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_v08_catalog_mapping_regression.py +0 -0
  154. {devagent_physical_engine-0.12.2 → devagent_physical_engine-0.12.4}/tests/test_workpiece_lifecycle_v011.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devagent-physical-engine
3
- Version: 0.12.2
3
+ Version: 0.12.4
4
4
  Summary: Verification-first agentic engineering, simulation, and evidence runtime for robotics and industrial automation.
5
5
  Author: Tom Ha
6
6
  License: All Rights Reserved
@@ -193,6 +193,10 @@ v0.12.1 hardens the initial ROS joint-state snapshot by parsing `ros2 topic echo
193
193
 
194
194
  v0.12.2 additionally hardens Gazebo model-pose read-back across Harmonic CLI renderings. One shared parser now accepts both pipe-delimited and whitespace-delimited finite XYZ/RPY vectors and is used by initial world verification and dynamic workpiece pose read-back, so task simulation does not depend on a single human-readable `gz model --pose` delimiter style.
195
195
 
196
+ v0.12.3 hardens MoveIt PlanningScene read-back when MoveIt serializes world collision objects in its own planning/root frame. A different observed frame is never trusted by label alone: DevAgent first proves the observed frame and canonical Twin frame are identity-equivalent through TF, then re-reads the scene and canonicalizes it back to the Twin planning frame before enforcing the exact geometry hash. Non-identity frames and geometry drift remain fail-closed.
197
+
198
+ v0.12.4 hardens attached-tool collision read-back against equivalent MoveIt `CollisionObject` pose decompositions. DevAgent now composes the object pose with the primitive pose, canonicalizes equivalent quaternion signs before hashing, and still requires the exact attachment link, object frame, shape, dimensions, operation, and effective geometry. Exact probe evidence is preserved beside the tool payload for target diagnostics; representation-only changes no longer create false mismatches, while physical geometry drift remains fail-closed.
199
+
196
200
  The packaged reference workcell is **not customer-site geometry**. Arbitrary plant layouts require an imported or user-declared physical Twin with trusted poses, collision geometry, tool/TCP information, and workpiece properties. DevAgent does not invent those site facts from a natural-language prompt.
197
201
 
198
202
  The v0.12 implementation remains evidence-gated: source/CI success does not prove the ROS target campaign has passed on a particular workstation. Use the executable qualification below before treating the task-specific path as qualified on that target.
@@ -420,7 +424,7 @@ Detailed engineering documentation lives in the repository:
420
424
 
421
425
  ## Project status
422
426
 
423
- **v0.12.2 — Beta software / evidence-gated task-specific physical simulation.**
427
+ **v0.12.4 — Beta software / evidence-gated task-specific physical simulation.**
424
428
 
425
429
  The software and release pipeline are production-oriented, but physical readiness is scope-specific and evidence-driven. The UR5e task-specific reference-workcell path must pass the executable v0.12 ROS/Gazebo/MoveIt campaign on the target workstation before that scope is considered simulation-qualified there. Real robot execution remains locked. Functional-safety certification and site qualification are not claimed.
426
430
 
@@ -152,6 +152,10 @@ v0.12.1 hardens the initial ROS joint-state snapshot by parsing `ros2 topic echo
152
152
 
153
153
  v0.12.2 additionally hardens Gazebo model-pose read-back across Harmonic CLI renderings. One shared parser now accepts both pipe-delimited and whitespace-delimited finite XYZ/RPY vectors and is used by initial world verification and dynamic workpiece pose read-back, so task simulation does not depend on a single human-readable `gz model --pose` delimiter style.
154
154
 
155
+ v0.12.3 hardens MoveIt PlanningScene read-back when MoveIt serializes world collision objects in its own planning/root frame. A different observed frame is never trusted by label alone: DevAgent first proves the observed frame and canonical Twin frame are identity-equivalent through TF, then re-reads the scene and canonicalizes it back to the Twin planning frame before enforcing the exact geometry hash. Non-identity frames and geometry drift remain fail-closed.
156
+
157
+ v0.12.4 hardens attached-tool collision read-back against equivalent MoveIt `CollisionObject` pose decompositions. DevAgent now composes the object pose with the primitive pose, canonicalizes equivalent quaternion signs before hashing, and still requires the exact attachment link, object frame, shape, dimensions, operation, and effective geometry. Exact probe evidence is preserved beside the tool payload for target diagnostics; representation-only changes no longer create false mismatches, while physical geometry drift remains fail-closed.
158
+
155
159
  The packaged reference workcell is **not customer-site geometry**. Arbitrary plant layouts require an imported or user-declared physical Twin with trusted poses, collision geometry, tool/TCP information, and workpiece properties. DevAgent does not invent those site facts from a natural-language prompt.
156
160
 
157
161
  The v0.12 implementation remains evidence-gated: source/CI success does not prove the ROS target campaign has passed on a particular workstation. Use the executable qualification below before treating the task-specific path as qualified on that target.
@@ -379,7 +383,7 @@ Detailed engineering documentation lives in the repository:
379
383
 
380
384
  ## Project status
381
385
 
382
- **v0.12.2 — Beta software / evidence-gated task-specific physical simulation.**
386
+ **v0.12.4 — Beta software / evidence-gated task-specific physical simulation.**
383
387
 
384
388
  The software and release pipeline are production-oriented, but physical readiness is scope-specific and evidence-driven. The UR5e task-specific reference-workcell path must pass the executable v0.12 ROS/Gazebo/MoveIt campaign on the target workstation before that scope is considered simulation-qualified there. Real robot execution remains locked. Functional-safety certification and site qualification are not claimed.
385
389
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devagent-physical-engine"
7
- version = "0.12.2"
7
+ version = "0.12.4"
8
8
  description = "Verification-first agentic engineering, simulation, and evidence runtime for robotics and industrial automation."
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  requires-python = ">=3.11"
@@ -41,4 +41,4 @@ __all__ = [
41
41
  "TwinValidator",
42
42
  ]
43
43
 
44
- __version__ = "0.12.2"
44
+ __version__ = "0.12.4"
@@ -2,12 +2,14 @@ from __future__ import annotations
2
2
 
3
3
  from dataclasses import asdict, dataclass
4
4
  import json
5
+ from math import isfinite
5
6
  from pathlib import Path
6
7
  from typing import Any
7
8
 
8
9
  from ..simulation_platform import SimulationPlatformError
9
10
  from ..twin_materialization import CanonicalTwinMaterialization
10
11
  from .commands import RosCommandRunner
12
+ from .frame_alignment import FrameAlignmentReceipt, FrameAlignmentVerifier
11
13
 
12
14
 
13
15
  @dataclass(frozen=True, slots=True)
@@ -21,6 +23,8 @@ class MoveItSceneReceipt:
21
23
  planning_frame: str
22
24
  expected_object_count: int
23
25
  observed_object_count: int | None
26
+ observed_planning_frames: tuple[str, ...] = ()
27
+ equivalent_planning_frames: tuple[str, ...] = ()
24
28
 
25
29
  def __post_init__(self) -> None:
26
30
  if not self.code.strip():
@@ -37,11 +41,44 @@ class MoveItSceneReceipt:
37
41
  raise ValueError("moveit_scene_receipt_object_count_invalid")
38
42
  if self.observed_object_count is not None and self.observed_object_count < 0:
39
43
  raise ValueError("moveit_scene_receipt_object_count_invalid")
44
+ if any(not value.strip() for value in self.observed_planning_frames):
45
+ raise ValueError("moveit_scene_receipt_observed_frame_invalid")
46
+ if any(not value.strip() for value in self.equivalent_planning_frames):
47
+ raise ValueError("moveit_scene_receipt_equivalent_frame_invalid")
40
48
 
41
49
  def to_dict(self) -> dict[str, Any]:
42
50
  return asdict(self)
43
51
 
44
52
 
53
+ def _equivalent_frames_from_receipt(
54
+ planning_frame: str,
55
+ frame_alignment: FrameAlignmentReceipt | None,
56
+ ) -> tuple[str, ...]:
57
+ """Return only frames already proven identity-equivalent to the canonical frame."""
58
+
59
+ if frame_alignment is None:
60
+ return ()
61
+ if (
62
+ not frame_alignment.verified
63
+ or not isfinite(frame_alignment.translation_error_m)
64
+ or not isfinite(frame_alignment.rotation_error_rad)
65
+ or frame_alignment.translation_error_m < 0.0
66
+ or frame_alignment.rotation_error_rad < 0.0
67
+ ):
68
+ raise SimulationPlatformError("moveit_scene_frame_receipt_invalid")
69
+
70
+ parent = frame_alignment.parent_frame.strip()
71
+ child = frame_alignment.child_frame.strip()
72
+ expected = planning_frame.strip()
73
+ if not parent or not child or not expected:
74
+ raise SimulationPlatformError("moveit_scene_frame_receipt_invalid")
75
+ if child == expected:
76
+ return () if parent == expected else (parent,)
77
+ if parent == expected:
78
+ return () if child == expected else (child,)
79
+ raise SimulationPlatformError("moveit_scene_frame_receipt_unbound")
80
+
81
+
45
82
  class MoveItSceneBridge:
46
83
  """Apply canonical Twin collision geometry to MoveIt and read it back."""
47
84
 
@@ -58,15 +95,19 @@ class MoveItSceneBridge:
58
95
  self.work_dir = Path(work_dir).expanduser().resolve()
59
96
  self.timeout_s = float(timeout_s)
60
97
 
61
- def apply_and_verify(
62
- self, materialization: CanonicalTwinMaterialization
63
- ) -> MoveItSceneReceipt:
64
- self.work_dir.mkdir(parents=True, exist_ok=True)
98
+ def _run_probe(
99
+ self,
100
+ *,
101
+ materialization: CanonicalTwinMaterialization,
102
+ equivalent_frames: tuple[str, ...],
103
+ ) -> tuple[dict[str, Any], bool]:
104
+ payload = materialization.moveit_scene_payload()
105
+ payload["equivalent_planning_frames"] = list(equivalent_frames)
65
106
  payload_path = self.work_dir / f"{materialization.fingerprint}.moveit-scene.json"
66
107
  temporary = payload_path.with_suffix(payload_path.suffix + ".tmp")
67
108
  temporary.write_text(
68
109
  json.dumps(
69
- materialization.moveit_scene_payload(),
110
+ payload,
70
111
  sort_keys=True,
71
112
  separators=(",", ":"),
72
113
  allow_nan=False,
@@ -93,8 +134,73 @@ class MoveItSceneBridge:
93
134
  raise SimulationPlatformError("moveit_scene_probe_output_invalid") from exc
94
135
  if not isinstance(output, dict):
95
136
  raise SimulationPlatformError("moveit_scene_probe_output_invalid")
137
+ return output, result.ok
138
+
139
+ def apply_and_verify(
140
+ self,
141
+ materialization: CanonicalTwinMaterialization,
142
+ *,
143
+ frame_alignment: FrameAlignmentReceipt | None = None,
144
+ ) -> MoveItSceneReceipt:
145
+ self.work_dir.mkdir(parents=True, exist_ok=True)
146
+ equivalent_frames = _equivalent_frames_from_receipt(
147
+ materialization.planning_frame,
148
+ frame_alignment,
149
+ )
150
+ output, command_ok = self._run_probe(
151
+ materialization=materialization,
152
+ equivalent_frames=equivalent_frames,
153
+ )
96
154
  code = str(output.get("code") or "moveit_scene_probe_failed")
97
- verified = bool(output.get("success")) and result.ok
155
+
156
+ # MoveIt serializes world collision objects in its own planning/root frame.
157
+ # If that label differs from the canonical Twin frame, do not trust it by
158
+ # convention. Prove the observed frame is identity-equivalent via TF, then
159
+ # repeat the read-back with that exact frame admitted for canonical hashing.
160
+ if (
161
+ frame_alignment is None
162
+ and not equivalent_frames
163
+ and not bool(output.get("success"))
164
+ and code.startswith("scene_planning_frame_mismatch:")
165
+ ):
166
+ observed_frames = tuple(
167
+ sorted(
168
+ {
169
+ str(value).strip()
170
+ for value in output.get("observed_planning_frames") or ()
171
+ if str(value).strip()
172
+ }
173
+ )
174
+ )
175
+ if len(observed_frames) != 1:
176
+ raise SimulationPlatformError(
177
+ "moveit_scene_observed_frame_ambiguous"
178
+ )
179
+ observed_frame = observed_frames[0]
180
+ receipt = FrameAlignmentVerifier(self.runner).verify_identity(
181
+ parent_frame=observed_frame,
182
+ child_frame=materialization.planning_frame,
183
+ )
184
+ equivalent_frames = _equivalent_frames_from_receipt(
185
+ materialization.planning_frame,
186
+ receipt,
187
+ )
188
+ output, command_ok = self._run_probe(
189
+ materialization=materialization,
190
+ equivalent_frames=equivalent_frames,
191
+ )
192
+ code = str(output.get("code") or "moveit_scene_probe_failed")
193
+
194
+ verified = bool(output.get("success")) and command_ok
195
+ observed_frames = tuple(
196
+ sorted(
197
+ {
198
+ str(value).strip()
199
+ for value in output.get("observed_planning_frames") or ()
200
+ if str(value).strip()
201
+ }
202
+ )
203
+ )
98
204
  receipt = MoveItSceneReceipt(
99
205
  verified=verified,
100
206
  code=code,
@@ -113,6 +219,8 @@ class MoveItSceneBridge:
113
219
  if output.get("observed_object_count") is not None
114
220
  else None
115
221
  ),
222
+ observed_planning_frames=observed_frames,
223
+ equivalent_planning_frames=equivalent_frames,
116
224
  )
117
225
  if not receipt.verified:
118
226
  raise SimulationPlatformError(f"moveit_scene_verification_failed:{receipt.code}")
@@ -35,6 +35,71 @@ def _canonical_scene(planning_frame: str, objects: list[dict[str, Any]]) -> dict
35
35
  }
36
36
 
37
37
 
38
+ def _verify_observed_scene(
39
+ *,
40
+ planning_frame: str,
41
+ expected_hash: str,
42
+ expected_objects: list[dict[str, Any]],
43
+ observed_objects: list[dict[str, Any]],
44
+ equivalent_planning_frames: tuple[str, ...] = (),
45
+ ) -> dict[str, Any]:
46
+ """Verify read-back without confusing an identity-equivalent frame label for drift.
47
+
48
+ ``equivalent_planning_frames`` is not discovered here. It must be supplied by the
49
+ caller from an independently verified identity transform receipt. Geometry is
50
+ always canonicalized back to ``planning_frame`` before hashing.
51
+ """
52
+
53
+ planning_frame = planning_frame.strip()
54
+ if not planning_frame:
55
+ raise ValueError("scene_planning_frame_required")
56
+ equivalent = tuple(value.strip() for value in equivalent_planning_frames)
57
+ if any(not value for value in equivalent):
58
+ raise ValueError("scene_equivalent_planning_frame_invalid")
59
+ accepted_frames = {planning_frame, *equivalent}
60
+ expected_ids = {str(item["id"]) for item in expected_objects}
61
+ relevant = [item for item in observed_objects if str(item["id"]) in expected_ids]
62
+ observed_frames = sorted({str(item.get("frame_id") or "").strip() for item in relevant})
63
+
64
+ for item in relevant:
65
+ frame_id = str(item.get("frame_id") or "").strip()
66
+ if frame_id not in accepted_frames:
67
+ return {
68
+ "success": False,
69
+ "code": f"scene_planning_frame_mismatch:{item['id']}:{frame_id or '<empty>'}",
70
+ "observed_object_count": len(relevant),
71
+ "observed_planning_frames": observed_frames,
72
+ }
73
+
74
+ canonical_objects = [
75
+ {
76
+ "id": item["id"],
77
+ "geometry_kind": item["geometry_kind"],
78
+ "dimensions_m": item["dimensions_m"],
79
+ "position_m": item["position_m"],
80
+ "quaternion_xyzw": item["quaternion_xyzw"],
81
+ }
82
+ for item in relevant
83
+ ]
84
+ observed_scene = _canonical_scene(planning_frame, canonical_objects)
85
+ observed_hash = _hash(observed_scene)
86
+ observed_ids = {str(item["id"]) for item in relevant}
87
+ missing = sorted(expected_ids - observed_ids)
88
+ success = (
89
+ not missing
90
+ and len(relevant) == len(expected_objects)
91
+ and observed_hash == expected_hash
92
+ )
93
+ return {
94
+ "success": success,
95
+ "code": "moveit_scene_readback_verified" if success else "moveit_scene_readback_mismatch",
96
+ "observed_scene_hash": observed_hash,
97
+ "observed_object_count": len(relevant),
98
+ "observed_planning_frames": observed_frames,
99
+ "missing_object_ids": missing,
100
+ }
101
+
102
+
38
103
  def _qmul(first, second):
39
104
  ax, ay, az, aw = first
40
105
  bx, by, bz, bw = second
@@ -78,6 +143,9 @@ def apply_verify(path: Path, timeout_s: float) -> dict[str, Any]:
78
143
  planning_frame = str(payload["planning_frame"])
79
144
  expected_hash = str(payload["scene_hash"])
80
145
  expected_objects = list(payload["objects"])
146
+ equivalent_planning_frames = tuple(
147
+ str(value) for value in payload.get("equivalent_planning_frames") or ()
148
+ )
81
149
 
82
150
  import rclpy
83
151
  from geometry_msgs.msg import Pose
@@ -155,13 +223,12 @@ def apply_verify(path: Path, timeout_s: float) -> dict[str, Any]:
155
223
  for collision in response.scene.world.collision_objects:
156
224
  if collision.id not in expected_ids:
157
225
  continue
158
- if collision.header.frame_id != planning_frame:
226
+ if len(collision.primitives) != 1 or len(collision.primitive_poses) != 1:
159
227
  return {
160
228
  "success": False,
161
- "code": f"scene_planning_frame_mismatch:{collision.id}:{collision.header.frame_id}",
229
+ "code": f"scene_shape_invalid:{collision.id}",
230
+ "observed_planning_frames": [collision.header.frame_id],
162
231
  }
163
- if len(collision.primitives) != 1 or len(collision.primitive_poses) != 1:
164
- return {"success": False, "code": f"scene_shape_invalid:{collision.id}"}
165
232
  primitive = collision.primitives[0]
166
233
  primitive_pose = collision.primitive_poses[0]
167
234
  position, quaternion = _combine_pose(collision.pose, primitive_pose)
@@ -175,26 +242,29 @@ def apply_verify(path: Path, timeout_s: float) -> dict[str, Any]:
175
242
  primitive.dimensions[SolidPrimitive.CYLINDER_HEIGHT],
176
243
  ]
177
244
  else:
178
- return {"success": False, "code": f"scene_shape_unsupported:{collision.id}"}
179
- observed.append({
180
- "id": collision.id,
181
- "geometry_kind": kind,
182
- "dimensions_m": dimensions,
183
- "position_m": list(position),
184
- "quaternion_xyzw": list(quaternion),
185
- })
186
-
187
- observed_scene = _canonical_scene(planning_frame, observed)
188
- observed_hash = _hash(observed_scene)
189
- missing = sorted(expected_ids - {item["id"] for item in observed})
190
- success = not missing and len(observed) == len(expected_objects) and observed_hash == expected_hash
191
- return {
192
- "success": success,
193
- "code": "moveit_scene_readback_verified" if success else "moveit_scene_readback_mismatch",
194
- "observed_scene_hash": observed_hash,
195
- "observed_object_count": len(observed),
196
- "missing_object_ids": missing,
197
- }
245
+ return {
246
+ "success": False,
247
+ "code": f"scene_shape_unsupported:{collision.id}",
248
+ "observed_planning_frames": [collision.header.frame_id],
249
+ }
250
+ observed.append(
251
+ {
252
+ "id": collision.id,
253
+ "frame_id": collision.header.frame_id,
254
+ "geometry_kind": kind,
255
+ "dimensions_m": dimensions,
256
+ "position_m": list(position),
257
+ "quaternion_xyzw": list(quaternion),
258
+ }
259
+ )
260
+
261
+ return _verify_observed_scene(
262
+ planning_frame=planning_frame,
263
+ expected_hash=expected_hash,
264
+ expected_objects=expected_objects,
265
+ observed_objects=observed,
266
+ equivalent_planning_frames=equivalent_planning_frames,
267
+ )
198
268
  finally:
199
269
  node.destroy_node()
200
270
  rclpy.shutdown()
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from hashlib import sha256
4
4
  import json
5
+ from math import isfinite, sqrt
5
6
  from pathlib import Path
6
7
 
7
8
  from ..simulation_platform import SimulationPlatformError
@@ -13,6 +14,8 @@ from .commands import RosCommandRunner
13
14
 
14
15
  def _canon_float(value: float) -> float:
15
16
  value = float(value)
17
+ if not isfinite(value):
18
+ raise SimulationPlatformError("tool_collision_geometry_non_finite")
16
19
  return 0.0 if abs(value) < 1e-12 else float(format(value, ".12g"))
17
20
 
18
21
 
@@ -21,6 +24,38 @@ def _hash(payload: object) -> str:
21
24
  return sha256(text.encode("utf-8")).hexdigest()
22
25
 
23
26
 
27
+ def _canonical_quaternion(values) -> tuple[float, float, float, float]:
28
+ quaternion = tuple(float(value) for value in values)
29
+ if len(quaternion) != 4 or any(not isfinite(value) for value in quaternion):
30
+ raise SimulationPlatformError("tool_collision_quaternion_invalid")
31
+ norm = sqrt(sum(value * value for value in quaternion))
32
+ if norm <= 0.0:
33
+ raise SimulationPlatformError("tool_collision_quaternion_invalid")
34
+ quaternion = tuple(value / norm for value in quaternion)
35
+ for component in (quaternion[3], quaternion[0], quaternion[1], quaternion[2]):
36
+ if abs(component) <= 1e-15:
37
+ continue
38
+ if component < 0.0:
39
+ quaternion = tuple(-value for value in quaternion)
40
+ break
41
+ return tuple(_canon_float(value) for value in quaternion)
42
+
43
+
44
+ def _canonical_tool_payload(
45
+ item: dict[str, object], binding: ToolCollisionBinding
46
+ ) -> dict[str, object]:
47
+ return {
48
+ "id": item["id"],
49
+ "link_name": binding.mount_link,
50
+ "geometry_kind": item["geometry_kind"],
51
+ "dimensions_m": [
52
+ _canon_float(value) for value in item["dimensions_m"] # type: ignore[index]
53
+ ],
54
+ "position_m": [_canon_float(value) for value in binding.position_m],
55
+ "quaternion_xyzw": list(_canonical_quaternion(binding.quaternion_xyzw)),
56
+ }
57
+
58
+
24
59
  class MoveItToolCollisionBridge:
25
60
  def __init__(
26
61
  self,
@@ -36,25 +71,19 @@ class MoveItToolCollisionBridge:
36
71
  self.timeout_s = float(timeout_s)
37
72
 
38
73
  @staticmethod
39
- def _geometry_payload(tool: ToolSpec, binding: ToolCollisionBinding) -> tuple[dict[str, object], str]:
74
+ def _geometry_payload(
75
+ tool: ToolSpec, binding: ToolCollisionBinding
76
+ ) -> tuple[dict[str, object], str]:
40
77
  if tool.geometry.kind not in {GeometryKind.BOX, GeometryKind.CYLINDER}:
41
78
  raise SimulationPlatformError("tool_collision_geometry_not_materializable")
42
79
  if not tool.geometry.collision_geometry:
43
80
  raise SimulationPlatformError("tool_collision_geometry_disabled")
44
- item = {
81
+ item: dict[str, object] = {
45
82
  "id": f"devagent_tool_{sha256(tool.tool_id.encode('utf-8')).hexdigest()[:12]}",
46
83
  "geometry_kind": tool.geometry.kind.value,
47
84
  "dimensions_m": [float(value) for value in tool.geometry.dimensions_m],
48
85
  }
49
- canonical = {
50
- "id": item["id"],
51
- "link_name": binding.mount_link,
52
- "geometry_kind": item["geometry_kind"],
53
- "dimensions_m": [_canon_float(value) for value in item["dimensions_m"]],
54
- "position_m": [_canon_float(value) for value in binding.position_m],
55
- "quaternion_xyzw": [_canon_float(value) for value in binding.quaternion_xyzw],
56
- }
57
- return item, _hash(canonical)
86
+ return item, _hash(_canonical_tool_payload(item, binding))
58
87
 
59
88
  def apply_and_verify(
60
89
  self,
@@ -76,10 +105,17 @@ class MoveItToolCollisionBridge:
76
105
  "tool_geometry_hash": geometry_hash,
77
106
  "binding_hash": binding.fingerprint,
78
107
  }
79
- payload_path = self.work_dir / f"{materialization.fingerprint}.{binding.fingerprint}.tool.json"
108
+ payload_path = self.work_dir / (
109
+ f"{materialization.fingerprint}.{binding.fingerprint}.tool.json"
110
+ )
80
111
  temporary = payload_path.with_suffix(payload_path.suffix + ".tmp")
81
112
  temporary.write_text(
82
- json.dumps(payload, sort_keys=True, separators=(",", ":"), allow_nan=False),
113
+ json.dumps(
114
+ payload,
115
+ sort_keys=True,
116
+ separators=(",", ":"),
117
+ allow_nan=False,
118
+ ),
83
119
  encoding="utf-8",
84
120
  )
85
121
  temporary.replace(payload_path)
@@ -101,6 +137,16 @@ class MoveItToolCollisionBridge:
101
137
  raise SimulationPlatformError("tool_scene_probe_output_invalid") from exc
102
138
  if not isinstance(output, dict):
103
139
  raise SimulationPlatformError("tool_scene_probe_output_invalid")
140
+
141
+ # Preserve the exact target read-back evidence even on failure. This keeps
142
+ # target-specific MoveIt serialization visible instead of collapsing it into
143
+ # a generic top-level qualification code.
144
+ output_path = payload_path.with_suffix(".probe.json")
145
+ output_path.write_text(
146
+ json.dumps(output, sort_keys=True, indent=2, allow_nan=False) + "\n",
147
+ encoding="utf-8",
148
+ )
149
+
104
150
  receipt = ToolCollisionReceipt(
105
151
  verified=bool(output.get("success")) and result.ok,
106
152
  code=str(output.get("code") or "tool_scene_probe_failed"),
@@ -121,7 +167,9 @@ class MoveItToolCollisionBridge:
121
167
  ),
122
168
  )
123
169
  if not receipt.verified:
124
- raise SimulationPlatformError(f"tool_collision_verification_failed:{receipt.code}")
170
+ raise SimulationPlatformError(
171
+ f"tool_collision_verification_failed:{receipt.code}"
172
+ )
125
173
  if receipt.observed_link != binding.mount_link:
126
174
  raise SimulationPlatformError("tool_collision_mount_link_mismatch")
127
175
  return receipt