topicforge 0.8.1__tar.gz → 0.9.0__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 (291) hide show
  1. {topicforge-0.8.1 → topicforge-0.9.0}/CHANGELOG.md +44 -1
  2. {topicforge-0.8.1 → topicforge-0.9.0}/PKG-INFO +15 -15
  3. {topicforge-0.8.1 → topicforge-0.9.0}/README.md +14 -14
  4. {topicforge-0.8.1 → topicforge-0.9.0}/docs/CLIENTS.md +17 -17
  5. {topicforge-0.8.1 → topicforge-0.9.0}/docs/CONTRACT.md +103 -2
  6. {topicforge-0.8.1 → topicforge-0.9.0}/docs/DDS_QUICKSTART.md +18 -19
  7. {topicforge-0.8.1 → topicforge-0.9.0}/docs/EVAL.md +75 -0
  8. {topicforge-0.8.1 → topicforge-0.9.0}/docs/TESTING.md +6 -6
  9. {topicforge-0.8.1 → topicforge-0.9.0}/docs/TOOLS.md +63 -29
  10. {topicforge-0.8.1 → topicforge-0.9.0}/docs/TROUBLESHOOTING.md +8 -13
  11. {topicforge-0.8.1 → topicforge-0.9.0}/docs/TUTORIEL.md +4 -4
  12. {topicforge-0.8.1 → topicforge-0.9.0}/docs/VALIDATION.md +1 -1
  13. {topicforge-0.8.1 → topicforge-0.9.0}/docs/dds-interop-matrix.md +3 -3
  14. {topicforge-0.8.1 → topicforge-0.9.0}/examples/README.md +1 -1
  15. {topicforge-0.8.1 → topicforge-0.9.0}/plugin/README.md +1 -1
  16. {topicforge-0.8.1 → topicforge-0.9.0}/plugin/pyproject.toml +2 -2
  17. {topicforge-0.8.1 → topicforge-0.9.0}/pyproject.toml +7 -11
  18. {topicforge-0.8.1 → topicforge-0.9.0}/scripts/agent_eval/README.md +3 -2
  19. {topicforge-0.8.1 → topicforge-0.9.0}/scripts/ground_truth/README.md +1 -0
  20. {topicforge-0.8.1 → topicforge-0.9.0}/scripts/integration/README.md +7 -7
  21. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/__init__.py +1 -1
  22. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/common/dds_helpers.py +2 -2
  23. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/common/dds_introspection.py +2 -1
  24. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/common/discovery_tracker.py +20 -2
  25. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/common/lifecycle.py +5 -5
  26. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/common/qos_endpoints.py +4 -4
  27. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/common/qos_normalize.py +4 -3
  28. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/common/xtypes.py +1 -1
  29. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/composite.py +1 -1
  30. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/dds_cyclone/adapter.py +1 -7
  31. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/ros2_live/adapter.py +2 -3
  32. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/config/settings.py +10 -6
  33. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/constants.py +6 -0
  34. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/models/__init__.py +5 -0
  35. topicforge-0.9.0/src/topicforge/models/diagnose.py +107 -0
  36. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/models/nodes.py +2 -2
  37. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/models/schemas.py +43 -50
  38. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/models/summaries.py +4 -5
  39. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/server/prompts.py +3 -2
  40. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/services/dds_views.py +1 -1
  41. topicforge-0.9.0/src/topicforge/services/diagnose.py +672 -0
  42. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/services/factory.py +2 -30
  43. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/services/health.py +1 -1
  44. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/services/inspector.py +5 -1
  45. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/tools/guard.py +3 -2
  46. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/tools/handlers.py +61 -24
  47. {topicforge-0.8.1 → topicforge-0.9.0}/tests/contract/_prompts.json +2 -2
  48. {topicforge-0.8.1 → topicforge-0.9.0}/tests/contract/analyze_bag.json +3 -3
  49. {topicforge-0.8.1 → topicforge-0.9.0}/tests/contract/detect_qos_mismatches.json +4 -4
  50. topicforge-0.9.0/tests/contract/diagnose.json +262 -0
  51. {topicforge-0.8.1 → topicforge-0.9.0}/tests/contract/get_topic_info.json +1 -1
  52. {topicforge-0.8.1 → topicforge-0.9.0}/tests/contract/graph_diff.json +1 -1
  53. {topicforge-0.8.1 → topicforge-0.9.0}/tests/contract/health_check.json +4 -4
  54. {topicforge-0.8.1 → topicforge-0.9.0}/tests/contract/inspect_tf.json +1 -1
  55. {topicforge-0.8.1 → topicforge-0.9.0}/tests/contract/list_endpoints.json +5 -5
  56. {topicforge-0.8.1 → topicforge-0.9.0}/tests/contract/list_nodes.json +2 -2
  57. {topicforge-0.8.1 → topicforge-0.9.0}/tests/contract/list_participants.json +5 -5
  58. {topicforge-0.8.1 → topicforge-0.9.0}/tests/contract/list_topics.json +1 -1
  59. {topicforge-0.8.1 → topicforge-0.9.0}/tests/contract/participant_events.json +3 -3
  60. {topicforge-0.8.1 → topicforge-0.9.0}/tests/contract/peek_bag_samples.json +5 -5
  61. {topicforge-0.8.1 → topicforge-0.9.0}/tests/contract/peek_dds_samples.json +6 -6
  62. {topicforge-0.8.1 → topicforge-0.9.0}/tests/contract/sample_messages.json +6 -6
  63. {topicforge-0.8.1 → topicforge-0.9.0}/tests/contract/topic_metrics.json +2 -2
  64. {topicforge-0.8.1 → topicforge-0.9.0}/tests/integration/ros2/entrypoint.sh +4 -3
  65. {topicforge-0.8.1 → topicforge-0.9.0}/tests/integration/ros2/ground_truth.py +43 -1
  66. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_agent_eval_tools.py +29 -0
  67. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_config.py +13 -31
  68. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_contract_shape.py +2 -1
  69. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_dds_cross_vendor.py +15 -39
  70. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_dds_inactive_reason.py +0 -6
  71. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_dds_introspection.py +1 -1
  72. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_dds_qos_normalization.py +1 -1
  73. topicforge-0.9.0/tests/test_diagnose.py +761 -0
  74. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_endpoints.py +52 -0
  75. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_factory.py +2 -16
  76. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_friction_fixes.py +27 -2
  77. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_ground_truth_compare.py +243 -3
  78. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_honest_outputs.py +1 -1
  79. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_http_transport.py +2 -2
  80. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_output_schema_validation.py +1 -0
  81. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_tool_annotations.py +2 -2
  82. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_tool_descriptions.py +3 -1
  83. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_tools_integration.py +5 -3
  84. topicforge-0.8.1/src/topicforge/adapters/dds_fast/__init__.py +0 -11
  85. topicforge-0.8.1/src/topicforge/adapters/dds_fast/adapter.py +0 -487
  86. topicforge-0.8.1/tests/test_fast_adapter.py +0 -149
  87. {topicforge-0.8.1 → topicforge-0.9.0}/.gitignore +0 -0
  88. {topicforge-0.8.1 → topicforge-0.9.0}/LICENSE +0 -0
  89. {topicforge-0.8.1 → topicforge-0.9.0}/docs/MIGRATION-0.7.md +0 -0
  90. {topicforge-0.8.1 → topicforge-0.9.0}/examples/dds/00_hello_pub_sub/README.md +0 -0
  91. {topicforge-0.8.1 → topicforge-0.9.0}/examples/dds/01_who_is_on_the_bus/README.md +0 -0
  92. {topicforge-0.8.1 → topicforge-0.9.0}/examples/dds/02_why_cant_they_talk/README.md +0 -0
  93. {topicforge-0.8.1 → topicforge-0.9.0}/examples/dds/03_a_node_crashed/README.md +0 -0
  94. {topicforge-0.8.1 → topicforge-0.9.0}/examples/dds/04_late_joiner_misses_data/README.md +0 -0
  95. {topicforge-0.8.1 → topicforge-0.9.0}/examples/dds/05_reliability_in_code/README.md +0 -0
  96. {topicforge-0.8.1 → topicforge-0.9.0}/examples/dds/06_durability_late_joiner_in_code/README.md +0 -0
  97. {topicforge-0.8.1 → topicforge-0.9.0}/examples/dds/07_deadline_in_code/README.md +0 -0
  98. {topicforge-0.8.1 → topicforge-0.9.0}/examples/dds/08_crash_seen_from_inside/README.md +0 -0
  99. {topicforge-0.8.1 → topicforge-0.9.0}/examples/dds/10_lidar_silent_after_driver_swap/README.md +0 -0
  100. {topicforge-0.8.1 → topicforge-0.9.0}/examples/dds/11_who_talks_to_whom/README.md +0 -0
  101. {topicforge-0.8.1 → topicforge-0.9.0}/examples/dds/12_safety_monitor_dropout/README.md +0 -0
  102. {topicforge-0.8.1 → topicforge-0.9.0}/examples/dds/13_deadline_not_offered/README.md +0 -0
  103. {topicforge-0.8.1 → topicforge-0.9.0}/examples/dds/14_restart_loop/README.md +0 -0
  104. {topicforge-0.8.1 → topicforge-0.9.0}/examples/dds/README.md +0 -0
  105. {topicforge-0.8.1 → topicforge-0.9.0}/plugin/LICENSE +0 -0
  106. {topicforge-0.8.1 → topicforge-0.9.0}/scripts/integration/publishers/cyclone_c/README.md +0 -0
  107. {topicforge-0.8.1 → topicforge-0.9.0}/scripts/integration/publishers/cyclone_cpp/README.md +0 -0
  108. {topicforge-0.8.1 → topicforge-0.9.0}/scripts/integration/publishers/cyclone_rust/README.md +0 -0
  109. {topicforge-0.8.1 → topicforge-0.9.0}/scripts/integration/publishers/dust_py/README.md +0 -0
  110. {topicforge-0.8.1 → topicforge-0.9.0}/scripts/integration/publishers/fast_publisher_cpp/README.md +0 -0
  111. {topicforge-0.8.1 → topicforge-0.9.0}/scripts/integration/publishers/fast_py/README.md +0 -0
  112. {topicforge-0.8.1 → topicforge-0.9.0}/scripts/integration/publishers/opensplice_publisher/README.md +0 -0
  113. {topicforge-0.8.1 → topicforge-0.9.0}/scripts/integration/publishers/rti_c/README.md +0 -0
  114. {topicforge-0.8.1 → topicforge-0.9.0}/scripts/integration/publishers/rti_cpp/README.md +0 -0
  115. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/__main__.py +0 -0
  116. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/__init__.py +0 -0
  117. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/base.py +0 -0
  118. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/common/__init__.py +0 -0
  119. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/common/bag_kind.py +0 -0
  120. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/common/bag_range.py +0 -0
  121. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/common/cdr_decoder.py +0 -0
  122. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/common/endpoints.py +0 -0
  123. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/common/log_filter.py +0 -0
  124. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/common/metrics_buffer.py +0 -0
  125. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/common/nodes.py +0 -0
  126. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/common/param_diff.py +0 -0
  127. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/common/qos_analyzer.py +0 -0
  128. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/common/qos_fixes.py +0 -0
  129. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/common/qos_scan.py +0 -0
  130. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/common/ros_discovery.py +0 -0
  131. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/common/ros_names.py +0 -0
  132. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/common/spawned_pids.py +0 -0
  133. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/common/stamps.py +0 -0
  134. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/common/tf_tree.py +0 -0
  135. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/common/topic_filter.py +0 -0
  136. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/common/urdf.py +0 -0
  137. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/dds_cyclone/__init__.py +0 -0
  138. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/dds_cyclone/ros_info.py +0 -0
  139. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/ros2_live/__init__.py +0 -0
  140. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/ros2_live/echo_parser.py +0 -0
  141. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/ros2_live/echo_stream.py +0 -0
  142. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/ros2_live/node_parsers.py +0 -0
  143. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/ros2_live/parsers.py +0 -0
  144. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/ros2_live/process_runner.py +0 -0
  145. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/ros2_live/process_tree.py +0 -0
  146. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/ros2_live/sample_extras.py +0 -0
  147. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/ros2_live/tf_capture.py +0 -0
  148. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/ros2_live/tf_parsers.py +0 -0
  149. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/ros2_mock/__init__.py +0 -0
  150. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/ros2_mock/adapter.py +0 -0
  151. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/ros2_mock/fixtures.py +0 -0
  152. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/ros2_mock/graph_fixtures.py +0 -0
  153. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/ros2_mock/node_fixtures.py +0 -0
  154. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/adapters/ros2_mock/tf_fixtures.py +0 -0
  155. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/budget.py +0 -0
  156. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/config/__init__.py +0 -0
  157. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/models/graph_diff.py +0 -0
  158. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/models/tf.py +0 -0
  159. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/server/__init__.py +0 -0
  160. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/server/app.py +0 -0
  161. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/server/http.py +0 -0
  162. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/services/__init__.py +0 -0
  163. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/services/bag_service.py +0 -0
  164. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/services/bag_stats.py +0 -0
  165. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/services/graph_diff.py +0 -0
  166. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/services/graph_snapshot.py +0 -0
  167. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/services/params_file.py +0 -0
  168. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/services/sample_budget.py +0 -0
  169. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/services/summaries/__init__.py +0 -0
  170. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/services/summaries/_common.py +0 -0
  171. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/services/summaries/clock.py +0 -0
  172. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/services/summaries/laser_scan.py +0 -0
  173. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/services/summaries/motion.py +0 -0
  174. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/services/summaries/rate.py +0 -0
  175. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/services/summaries/sensor_data.py +0 -0
  176. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/telemetry/__init__.py +0 -0
  177. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/telemetry/client.py +0 -0
  178. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/tools/__init__.py +0 -0
  179. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/tools/annotations.py +0 -0
  180. {topicforge-0.8.1 → topicforge-0.9.0}/src/topicforge/tools/schema_compact.py +0 -0
  181. {topicforge-0.8.1 → topicforge-0.9.0}/tests/__init__.py +0 -0
  182. {topicforge-0.8.1 → topicforge-0.9.0}/tests/conftest.py +0 -0
  183. {topicforge-0.8.1 → topicforge-0.9.0}/tests/contract/get_node_info.json +0 -0
  184. {topicforge-0.8.1 → topicforge-0.9.0}/tests/fixtures/ground_truth/omnisim_064_results.json +0 -0
  185. {topicforge-0.8.1 → topicforge-0.9.0}/tests/fixtures/ground_truth/omnisim_064_truth.json +0 -0
  186. {topicforge-0.8.1 → topicforge-0.9.0}/tests/fixtures/ground_truth/omnisim_071_morning_results.json +0 -0
  187. {topicforge-0.8.1 → topicforge-0.9.0}/tests/fixtures/ground_truth/omnisim_071_morning_truth.json +0 -0
  188. {topicforge-0.8.1 → topicforge-0.9.0}/tests/fixtures/ground_truth/omnisim_071_rerun_results.json +0 -0
  189. {topicforge-0.8.1 → topicforge-0.9.0}/tests/fixtures/ground_truth/omnisim_071_rerun_truth.json +0 -0
  190. {topicforge-0.8.1 → topicforge-0.9.0}/tests/fixtures/ros2_echo/image_noarr.yaml +0 -0
  191. {topicforge-0.8.1 → topicforge-0.9.0}/tests/fixtures/ros2_echo/image_trunc4.yaml +0 -0
  192. {topicforge-0.8.1 → topicforge-0.9.0}/tests/fixtures/ros2_echo/scan_edge.yaml +0 -0
  193. {topicforge-0.8.1 → topicforge-0.9.0}/tests/fixtures/ros2_echo/scan_noarr.yaml +0 -0
  194. {topicforge-0.8.1 → topicforge-0.9.0}/tests/fixtures/ros2_echo/scan_trunc128.yaml +0 -0
  195. {topicforge-0.8.1 → topicforge-0.9.0}/tests/fixtures/ros2_echo/scan_trunc3.yaml +0 -0
  196. {topicforge-0.8.1 → topicforge-0.9.0}/tests/fixtures/ros2_echo/string_latched.yaml +0 -0
  197. {topicforge-0.8.1 → topicforge-0.9.0}/tests/fixtures/ros2_echo/twist.yaml +0 -0
  198. {topicforge-0.8.1 → topicforge-0.9.0}/tests/fixtures/ros2_node/node_info_humble.txt +0 -0
  199. {topicforge-0.8.1 → topicforge-0.9.0}/tests/fixtures/ros2_node/node_info_no_param_services.txt +0 -0
  200. {topicforge-0.8.1 → topicforge-0.9.0}/tests/fixtures/ros2_node/node_info_not_found.txt +0 -0
  201. {topicforge-0.8.1 → topicforge-0.9.0}/tests/fixtures/ros2_node/node_list_duplicates.txt +0 -0
  202. {topicforge-0.8.1 → topicforge-0.9.0}/tests/fixtures/ros2_node/node_list_empty.txt +0 -0
  203. {topicforge-0.8.1 → topicforge-0.9.0}/tests/fixtures/ros2_node/node_list_plain.txt +0 -0
  204. {topicforge-0.8.1 → topicforge-0.9.0}/tests/fixtures/ros2_node/node_list_warning_stdout.txt +0 -0
  205. {topicforge-0.8.1 → topicforge-0.9.0}/tests/fixtures/ros2_node/param_dump_humble.yaml +0 -0
  206. {topicforge-0.8.1 → topicforge-0.9.0}/tests/fixtures/ros2_node/param_dump_secrets.yaml +0 -0
  207. {topicforge-0.8.1 → topicforge-0.9.0}/tests/fixtures/ros2_node/service_list_types.txt +0 -0
  208. {topicforge-0.8.1 → topicforge-0.9.0}/tests/fixtures/ros2_topic_info_verbose_clock.txt +0 -0
  209. {topicforge-0.8.1 → topicforge-0.9.0}/tests/fixtures/ros2_topic_info_verbose_cmd_vel.txt +0 -0
  210. {topicforge-0.8.1 → topicforge-0.9.0}/tests/fixtures/ros2_topic_info_verbose_parameter_events.txt +0 -0
  211. {topicforge-0.8.1 → topicforge-0.9.0}/tests/fixtures/ros2_topic_info_verbose_scan.txt +0 -0
  212. {topicforge-0.8.1 → topicforge-0.9.0}/tests/fixtures/ros2_topic_info_verbose_tf.txt +0 -0
  213. {topicforge-0.8.1 → topicforge-0.9.0}/tests/fixtures/ros2_topic_info_verbose_tf_static.txt +0 -0
  214. {topicforge-0.8.1 → topicforge-0.9.0}/tests/fixtures/ros_discovery/humble_two_nodes.hex +0 -0
  215. {topicforge-0.8.1 → topicforge-0.9.0}/tests/fixtures/ros_discovery/jazzy_two_nodes.hex +0 -0
  216. {topicforge-0.8.1 → topicforge-0.9.0}/tests/integration/__init__.py +0 -0
  217. {topicforge-0.8.1 → topicforge-0.9.0}/tests/integration/ros2/Dockerfile +0 -0
  218. {topicforge-0.8.1 → topicforge-0.9.0}/tests/integration/ros2/__init__.py +0 -0
  219. {topicforge-0.8.1 → topicforge-0.9.0}/tests/integration/ros2/bench_params.yaml +0 -0
  220. {topicforge-0.8.1 → topicforge-0.9.0}/tests/integration/ros2/blocked_node.py +0 -0
  221. {topicforge-0.8.1 → topicforge-0.9.0}/tests/integration/ros2/fault_nodes.py +0 -0
  222. {topicforge-0.8.1 → topicforge-0.9.0}/tests/integration/ros2/faults.py +0 -0
  223. {topicforge-0.8.1 → topicforge-0.9.0}/tests/integration/ros2/graph_change.py +0 -0
  224. {topicforge-0.8.1 → topicforge-0.9.0}/tests/integration/ros2/lifecycle_node.py +0 -0
  225. {topicforge-0.8.1 → topicforge-0.9.0}/tests/integration/ros2/publisher.py +0 -0
  226. {topicforge-0.8.1 → topicforge-0.9.0}/tests/integration/ros2/run_bench.py +0 -0
  227. {topicforge-0.8.1 → topicforge-0.9.0}/tests/integration/ros2/sros2_check.py +0 -0
  228. {topicforge-0.8.1 → topicforge-0.9.0}/tests/integration/ros2/test_live_adapter.py +0 -0
  229. {topicforge-0.8.1 → topicforge-0.9.0}/tests/integration/test_real_bus.py +0 -0
  230. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_analyze_bag_multi_format.py +0 -0
  231. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_bag_omnisim_humble.py +0 -0
  232. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_bag_service.py +0 -0
  233. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_bag_stats.py +0 -0
  234. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_bag_summaries.py +0 -0
  235. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_cdr_decoder.py +0 -0
  236. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_clock_basis.py +0 -0
  237. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_composite_adapter.py +0 -0
  238. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_composite_ghosts.py +0 -0
  239. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_contract_snapshots.py +0 -0
  240. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_contract_v2.py +0 -0
  241. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_cyclone_adapter.py +0 -0
  242. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_cyclone_ros_info.py +0 -0
  243. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_dds_helpers.py +0 -0
  244. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_dds_schemas.py +0 -0
  245. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_discovery_tracker.py +0 -0
  246. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_echo_parser.py +0 -0
  247. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_echo_stream.py +0 -0
  248. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_example_node_spec.py +0 -0
  249. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_field_names.py +0 -0
  250. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_graph_diff.py +0 -0
  251. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_health.py +0 -0
  252. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_history_and_hints.py +0 -0
  253. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_inspector.py +0 -0
  254. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_lifecycle_buffer.py +0 -0
  255. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_live_adapter_graph.py +0 -0
  256. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_live_adapter_parse.py +0 -0
  257. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_live_adapter_subprocess.py +0 -0
  258. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_live_sample_notes.py +0 -0
  259. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_log_filter.py +0 -0
  260. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_metrics_buffer.py +0 -0
  261. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_mock_adapter.py +0 -0
  262. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_node_logic.py +0 -0
  263. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_node_parsers.py +0 -0
  264. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_node_tools.py +0 -0
  265. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_param_diff.py +0 -0
  266. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_peek_bag_samples.py +0 -0
  267. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_process_runner.py +0 -0
  268. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_process_tree.py +0 -0
  269. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_prompts.py +0 -0
  270. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_qos_analyzer.py +0 -0
  271. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_qos_endpoints.py +0 -0
  272. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_qos_fixes.py +0 -0
  273. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_qos_scan.py +0 -0
  274. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_rate_verdicts.py +0 -0
  275. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_restarts_by_node.py +0 -0
  276. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_ros_discovery.py +0 -0
  277. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_sample_options.py +0 -0
  278. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_sample_summary_flow.py +0 -0
  279. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_server_guard.py +0 -0
  280. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_server_info_version.py +0 -0
  281. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_server_lanes.py +0 -0
  282. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_stamps.py +0 -0
  283. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_summaries.py +0 -0
  284. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_summary_schema_compat.py +0 -0
  285. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_telemetry.py +0 -0
  286. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_tf_tools.py +0 -0
  287. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_tf_tree.py +0 -0
  288. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_topic_metrics.py +0 -0
  289. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_urdf.py +0 -0
  290. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_version_consistency.py +0 -0
  291. {topicforge-0.8.1 → topicforge-0.9.0}/tests/test_xtypes.py +0 -0
@@ -5,6 +5,48 @@ All notable changes to TopicForge are documented in this file.
5
5
  The format is based on [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ## [0.9.0] - 2026-11-02
11
+
12
+ ### Added
13
+
14
+ - `diagnose`, the seventeenth tool: one bounded sweep of the ROS 2 graph and the DDS bus that
15
+ answers "is anything wrong". It runs `environment`, `nodes`, `topics`, `dds`, `tf`, `logs` and
16
+ `responsiveness` (a parameter read per node, the `node` argument first) from the existing
17
+ services, within `budget_s` (10..40, default 30), and returns findings ranked `critical`,
18
+ `warning`, `info` with evidence and, for QoS, the `suggested_fixes` of `detect_qos_mismatches`.
19
+ A check that errors or runs out of budget is listed in `checks_skipped` and the verdict then
20
+ says the sweep is incomplete: it never reads as an all-clear. It holds the ROS lock for up to
21
+ `budget_s`. `tools/list` is 99,634 bytes (`diagnose` 4,267) against the 105,000 ceiling. The
22
+ bench gains `50_diagnose` and `51_diagnose_node` and a comparator section that requires the
23
+ exact set of seeded findings (`docs/CONTRACT.md` section 9.21).
24
+
25
+ ### Changed
26
+
27
+ - `tools/list` is 95,391 bytes (-1,924): shorter lane notes, no duplicated "Read-only" phrase,
28
+ and every output field description at most 120 characters, with a test that keeps it so. The
29
+ `tools/list` test ceiling moves from 100,000 to 105,000 bytes to make room for `diagnose`
30
+ (`docs/CONTRACT.md` section 9.20).
31
+
32
+ ### Removed
33
+
34
+ - The fastdds-based DDS adapter (`adapters/dds_fast/`, 487 lines). It never ran against a bus,
35
+ and its `fastdds` binding is not on PyPI, so no user could install it. Fast DDS participants
36
+ are still observed through the `cyclone` backend by standard RTPS discovery, as the CI bench
37
+ and the OmniSim kit show. `TOPICFORGE_DDS_BACKEND=fast` is now refused at startup with that
38
+ route, and `auto` only probes `cyclonedds`. `health_check.dds_backend` keeps `fast` in its
39
+ schema enum but never emits it. `demo-fast.yml` (a C++ Fast DDS publisher seen by Cyclone)
40
+ stays.
41
+
42
+ ### Fixed
43
+
44
+ - `detect_qos_mismatches` on a live DDS backend now fills `reader_node` and `writer_node`. 0.8.1
45
+ built the scan's endpoints without the gid -> node map that `list_endpoints` uses, so both
46
+ fields stayed `null` on every pair (seen on Fast DDS in the OmniSim kit). Both tools now build
47
+ their endpoints through one snapshot method. The Docker bench checks the writer node on the
48
+ scan's pairs (`docs/CONTRACT.md` section 9.19).
49
+
8
50
  ## [0.8.1] - 2026-10-26
9
51
 
10
52
  The 0.8.0 evaluation findings, a URDF read from the robot_state_publisher parameter, an optional
@@ -1901,7 +1943,8 @@ Initial MVP release of TopicForge: ROS Topic Inspector & Bag Analyzer MCP server
1901
1943
  - The write path (publishing, commanding robots) is intentionally out of scope for the MVP.
1902
1944
  - `analyze_bag` in live mode parses `ros2 bag info` text output; deeper anomaly detection remains mock-only for now.
1903
1945
 
1904
- [Unreleased]: https://github.com/yaniswav/TopicForge/compare/v0.8.1...HEAD
1946
+ [Unreleased]: https://github.com/yaniswav/TopicForge/compare/v0.9.0...HEAD
1947
+ [0.9.0]: https://github.com/yaniswav/TopicForge/compare/v0.8.1...v0.9.0
1905
1948
  [0.8.1]: https://github.com/yaniswav/TopicForge/compare/v0.8.0...v0.8.1
1906
1949
  [0.8.0]: https://github.com/yaniswav/TopicForge/compare/v0.7.2...v0.8.0
1907
1950
  [0.7.2]: https://github.com/yaniswav/TopicForge/compare/v0.7.1...v0.7.2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: topicforge
3
- Version: 0.8.1
3
+ Version: 0.9.0
4
4
  Summary: ROS Topic Inspector & Bag Analyzer MCP server for AI agents
5
5
  Project-URL: Homepage, https://github.com/yaniswav/TopicForge
6
6
  Project-URL: Repository, https://github.com/yaniswav/TopicForge
@@ -73,7 +73,7 @@ A read-only MCP (Model Context Protocol) server that lets an AI agent inspect a
73
73
 
74
74
  Every tool also declares itself read-only in the MCP protocol (`readOnlyHint`, `destructiveHint=false`, `idempotentHint`, with an honest `openWorldHint`), and a test fails if any tool lacks it. See [`SECURITY.md`](SECURITY.md#read-only-guarantee-architecture-declaration-proof) for the guarantee and its limits.
75
75
 
76
- It gives the agent sixteen typed tools that return frozen Pydantic schemas (output contract 2: one object per tool, one `note`, units in the field names; see [`docs/CONTRACT.md`](docs/CONTRACT.md)), identical whether the server talks to a real robot or to its built-in mock fixtures. Ask why `nav_planner` gets no scan, and the agent reads the bus, finds the BEST_EFFORT writer facing a RELIABLE reader and names the incompatible policy (see [`examples/02-debug-qos-mismatch.md`](examples/02-debug-qos-mismatch.md)). It is meant for ROS2 developers, robotics ML/CV engineers and teams that cannot accept a write path into a production stack.
76
+ It gives the agent seventeen typed tools that return frozen Pydantic schemas (output contract 2: one object per tool, one `note`, units in the field names; see [`docs/CONTRACT.md`](docs/CONTRACT.md)), identical whether the server talks to a real robot or to its built-in mock fixtures. Ask why `nav_planner` gets no scan, and the agent reads the bus, finds the BEST_EFFORT writer facing a RELIABLE reader and names the incompatible policy (see [`examples/02-debug-qos-mismatch.md`](examples/02-debug-qos-mismatch.md)). It is meant for ROS2 developers, robotics ML/CV engineers and teams that cannot accept a write path into a production stack.
77
77
 
78
78
  For DDS, TopicForge joins a domain as a read-only participant through one open-source binding (Eclipse CycloneDDS from PyPI) and reads the builtin discovery topics that the OMG DDS-RTPS protocol standardizes. Cyclone DDS, Fast DDS and Dust DDS participants have been observed on live buses: the Fast DDS ones on every CI run of the ROS 2 test bench (Humble and Jazzy with `rmw_fastrtps_cpp`) and in the OmniSim validation. RTI Connext, OpenDDS and CoreDX announce themselves through the same standard discovery, but none of them has been observed yet. This covers discovery only: participants, readers, writers and their QoS. See [`docs/dds-interop-matrix.md`](docs/dds-interop-matrix.md).
79
79
 
@@ -103,7 +103,7 @@ The server speaks MCP over stdio and waits for a client, so wire it into one. Fo
103
103
 
104
104
  Then ask it to list the topics or to analyze `/tmp/demo.mcap`. For Claude Code: `claude mcp add topicforge -- topicforge`. If the client cannot launch the server (WSL2 to Windows, a robot over an SSH tunnel), `topicforge --transport streamable-http --port 8765` serves `http://127.0.0.1:8765/mcp` on loopback (another `--host` needs a bearer token in `TOPICFORGE_HTTP_TOKEN`); see [`docs/CLIENTS.md`](docs/CLIENTS.md#local-http-transport).
105
105
 
106
- **Other MCP clients.** Ready-to-paste configs for Claude Desktop, Cursor, VS Code, Windsurf, Cline, Roo Code, Continue, Zed, JetBrains, Codex, Gemini CLI, Goose and more are in [`docs/CLIENTS.md`](docs/CLIENTS.md). One-click: [Add to Cursor](https://cursor.com/install-mcp?name=topicforge&config=eyJjb21tYW5kIjoidXZ4IiwiYXJncyI6WyItLWZyb20iLCJ0b3BpY2ZvcmdlPT0wLjguMSIsInRvcGljZm9yZ2UiXSwiZW52Ijp7IlRPUElDRk9SR0VfTU9ERSI6ImF1dG8iLCJUT1BJQ0ZPUkdFX0REU19CQUNLRU5EIjoiY3ljbG9uZSIsIlRPUElDRk9SR0VfRERTX0RPTUFJTl9JRCI6IjAifX0%3D) | [Install in VS Code](https://vscode.dev/redirect/mcp/install?name=topicforge&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22--from%22%2C%22topicforge%3D%3D0.8.1%22%2C%22topicforge%22%5D%2C%22env%22%3A%7B%22TOPICFORGE_MODE%22%3A%22auto%22%2C%22TOPICFORGE_DDS_BACKEND%22%3A%22cyclone%22%2C%22TOPICFORGE_DDS_DOMAIN_ID%22%3A%220%22%7D%7D).
106
+ **Other MCP clients.** Ready-to-paste configs for Claude Desktop, Cursor, VS Code, Windsurf, Cline, Roo Code, Continue, Zed, JetBrains, Codex, Gemini CLI, Goose and more are in [`docs/CLIENTS.md`](docs/CLIENTS.md). One-click: [Add to Cursor](https://cursor.com/install-mcp?name=topicforge&config=eyJjb21tYW5kIjoidXZ4IiwiYXJncyI6WyItLWZyb20iLCJ0b3BpY2ZvcmdlPT0wLjkuMCIsInRvcGljZm9yZ2UiXSwiZW52Ijp7IlRPUElDRk9SR0VfTU9ERSI6ImF1dG8iLCJUT1BJQ0ZPUkdFX0REU19CQUNLRU5EIjoiY3ljbG9uZSIsIlRPUElDRk9SR0VfRERTX0RPTUFJTl9JRCI6IjAifX0%3D) | [Install in VS Code](https://vscode.dev/redirect/mcp/install?name=topicforge&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22--from%22%2C%22topicforge%3D%3D0.9.0%22%2C%22topicforge%22%5D%2C%22env%22%3A%7B%22TOPICFORGE_MODE%22%3A%22auto%22%2C%22TOPICFORGE_DDS_BACKEND%22%3A%22cyclone%22%2C%22TOPICFORGE_DDS_DOMAIN_ID%22%3A%220%22%7D%7D).
107
107
 
108
108
  Setup for a real ROS2 environment (WSL2, Linux, Docker, native Windows) is in [`docs/TESTING.md`](docs/TESTING.md); recurring monitoring prompts and the privacy contract are in [`docs/TUTORIEL.md`](docs/TUTORIEL.md).
109
109
 
@@ -120,6 +120,7 @@ Every response except `health_check` carries `mode_effective` (`"live"` or `"moc
120
120
  | `get_node_info` | One node's publishers, subscribers, services, actions, parameters (secrets masked) and `use_sim_time`; a node that does not answer is reported, not an error |
121
121
  | `inspect_tf` | The TF tree from `/tf_static` and `/tf`: roots, stale and two-parent frames, disconnected trees, sim-time-safe ages and rates, optional path between two frames |
122
122
  | `graph_diff` | What changed since it last worked: snapshots the ROS 2 graph (or a bag) and diffs it with `previous`, a `snap-N` id or a bag: nodes, topics, types, writer QoS, counts, bag rates |
123
+ | `diagnose` | Is anything wrong: one bounded sweep (nodes, topics, QoS pairs, restarts, TF, `/rosout` errors, a parameter read per node) ranked `critical` / `warning` / `info` with evidence; a check that cannot run is listed, never silent. Holds the ROS lock up to `budget_s` |
123
124
  | `sample_messages` | Peek recent messages on a ROS2 topic (count capped at 50) |
124
125
  | `analyze_bag` | Summarize a `.mcap` / `.db3` recording or `rosbag2_*` directory (via `ros2 bag info`) |
125
126
  | `list_participants` | DDS participants on the domain: vendor, `name` (EntityName QoS, Cyclone) and `hostname` |
@@ -152,9 +153,9 @@ pip install "topicforge[dds]" # alias, kept so older commands
152
153
  TOPICFORGE_DDS_BACKEND=cyclone python -m topicforge
153
154
  ```
154
155
 
155
- `TOPICFORGE_DDS_BACKEND` accepts `mock` (default), `cyclone`, `fast` and `auto` (`fast`, then `cyclone`, then `mock`, whichever binding imports). The default `mock` selects no DDS backend: installing the Cyclone binding is not enough, you must also set `TOPICFORGE_DDS_BACKEND=cyclone`. With `TOPICFORGE_MODE=live` and no backend selected, the DDS tools raise `DDS module is not active: ...` with the actual cause (backend not selected, binding not installed, or binding installed but the adapter failed to start), and `health_check` reports `dds_backend: "none"` plus `dds_inactive_note`. An explicit value is honoured with or without `ros2` on PATH, in any mode except `mock`. If the binding is missing or the participant cannot start, the server logs a warning naming the cause and falls back to the ROS2 CLI alone, or to the mock fixtures. When both `ros2` and a DDS backend are up, a composite adapter routes the eight `ros2` CLI tools (`list_topics`, `get_topic_info`, `list_nodes`, `get_node_info`, `inspect_tf`, `graph_diff`, `sample_messages`, `analyze_bag`) to the CLI and the six DDS tools to the DDS backend; `peek_bag_samples` reads bags without the CLI.
156
+ `TOPICFORGE_DDS_BACKEND` accepts `mock` (default), `cyclone` and `auto` (`cyclone` if the binding imports, else `mock`). The default `mock` selects no DDS backend: installing the Cyclone binding is not enough, you must also set `TOPICFORGE_DDS_BACKEND=cyclone`. With `TOPICFORGE_MODE=live` and no backend selected, the DDS tools raise `DDS module is not active: ...` with the actual cause (backend not selected, binding not installed, or binding installed but the adapter failed to start), and `health_check` reports `dds_backend: "none"` plus `dds_inactive_note`. An explicit value is honoured with or without `ros2` on PATH, in any mode except `mock`. If the binding is missing or the participant cannot start, the server logs a warning naming the cause and falls back to the ROS2 CLI alone, or to the mock fixtures. When both `ros2` and a DDS backend are up, a composite adapter routes the nine `ros2` CLI tools (`list_topics`, `get_topic_info`, `list_nodes`, `get_node_info`, `inspect_tf`, `graph_diff`, `diagnose`, `sample_messages`, `analyze_bag`) to the CLI and the six DDS tools to the DDS backend; `peek_bag_samples` goes to the ROS half but runs no `ros2` process.
156
157
 
157
- A Fast DDS adapter exists but has never run against a bus, and its `fastdds` Python binding is not on PyPI: build it from eProsima's sources and install it next to TopicForge. There is no `[dds-fast]` extra. Any other value (`rti`, `opensplice`, `coredx`, `intercom`, `opendds`, `dust`) is rejected at startup with an error that lists the valid ones; Cyclone already sees those vendors' participants through standard discovery. Full backend selection, the routing table and the QoS mismatch scenario are in [`docs/DDS_QUICKSTART.md`](docs/DDS_QUICKSTART.md); error messages are in [`docs/TROUBLESHOOTING.md`](docs/TROUBLESHOOTING.md).
158
+ Fast DDS participants (ROS 2 nodes on `rmw_fastrtps_cpp`) need no backend of their own: the Cyclone backend observes them through standard RTPS discovery. Since 0.9.0 `TOPICFORGE_DDS_BACKEND=fast` is refused, like `rti`, `opensplice`, `coredx`, `intercom`, `opendds` and `dust`: startup stops with an error that lists the valid values (see [`docs/TROUBLESHOOTING.md`](docs/TROUBLESHOOTING.md)). Full backend selection, the routing table and the QoS mismatch scenario are in [`docs/DDS_QUICKSTART.md`](docs/DDS_QUICKSTART.md).
158
159
 
159
160
  ## Configuration reference
160
161
 
@@ -164,7 +165,7 @@ A Fast DDS adapter exists but has never run against a bus, and its `fastdds` Pyt
164
165
  | `TOPICFORGE_LOG_LEVEL` | `INFO` | `DEBUG`, `INFO`, `WARNING`, `ERROR` |
165
166
  | `TOPICFORGE_ROS2_BIN` | `ros2` | Name or path of the ROS2 CLI binary |
166
167
  | `TOPICFORGE_TELEMETRY` | `off` | Opt-in anonymous telemetry; an unrecognized value aborts startup. See [Telemetry](#telemetry) |
167
- | `TOPICFORGE_DDS_BACKEND` | `mock` | `mock` (no DDS backend), `cyclone`, `fast`, `auto`; anything else is rejected |
168
+ | `TOPICFORGE_DDS_BACKEND` | `mock` | `mock` (no DDS backend), `cyclone`, `auto`; anything else is rejected |
168
169
  | `TOPICFORGE_DDS_DOMAIN_ID` | `0` | DDS domain observed (0..232). Joined at startup; changing it needs a restart |
169
170
  | `TOPICFORGE_MAX_SAMPLE_BYTES` | `1048576` | Size cap for one sampled message (1 KiB..64 MiB); a call returns at most 4 times that. Over-cap messages are dropped with a note |
170
171
 
@@ -176,19 +177,18 @@ Samples with comments are in [`.env.example`](.env.example). Any invalid value s
176
177
 
177
178
  External validation against a simulated robot's ground truth (OmniSim): [docs/VALIDATION.md](docs/VALIDATION.md).
178
179
 
179
- - DDS validation is partial. The Cyclone adapter has run against real buses with Cyclone, Fast DDS and Dust DDS participants, on Windows and in CI on Ubuntu and Windows (`.github/workflows/demo.yml`, `.github/workflows/ros2-live.yml`). The Fast DDS adapter (TopicForge joining through the `fastdds` binding) has never run against a bus, and no RTI, OpenDDS, CoreDX or OpenSplice participant has been observed by this project. The multi-vendor claim rests on the RTPS protocol guarantee, not on a recorded cross-vendor run.
180
+ - DDS validation is partial. The Cyclone adapter has run against real buses with Cyclone, Fast DDS and Dust DDS participants, on Windows and in CI on Ubuntu and Windows (`.github/workflows/demo.yml`, `.github/workflows/ros2-live.yml`). No RTI, OpenDDS, CoreDX or OpenSplice participant has been observed by this project. The multi-vendor claim rests on the RTPS protocol guarantee, not on a recorded cross-vendor run.
180
181
  - User-topic payloads are not decoded. `peek_dds_samples` on a user topic returns count 0 and a note that the topic is announced on the bus; no traffic is read. `topic_metrics` therefore has data only for the builtin discovery topics and says so in its `status`. It is a discovery-layer probe, not a publish-rate monitor.
181
182
  - Liveliness at runtime is not observed. A writer that is alive but silent (a hung process whose lease is still renewed) looks healthy, because TopicForge reads discovery, not data. An opt-in data probe is planned. A crash and a clean leave cannot be told apart, and `lost_ns` is an upper bound of the death.
182
183
  - Cyclone vendor ids: participants that do not follow the RTPS vendor-id convention in their GUID prefix (Dust DDS, and RTI by default) are reported with vendor `unknown`.
183
184
  - Single domain: the server observes the domain it joined at startup; changing it needs a restart.
184
185
  - DDS Security is not handled. A participant without credentials sees an empty secure bus. `detect_qos_mismatches` checks Partition, type name, Reliability, Durability, Deadline, Liveliness, LatencyBudget, Ownership (kind), DestinationOrder and DataRepresentation (History as a risk); Presentation, XTypes assignability and runtime behavior are not checked, and the result lists them in `policies_unchecked`. It returns a `MismatchScan` envelope: read `reports` for the mismatches.
185
- - Fast DDS serves no `list_endpoints`.
186
186
  - `sample_messages` (live) streams `ros2 topic echo` until `count` messages arrive or `timeout_s` (1..40, default 10, for the whole call) runs out, and returns what arrived with a `note` (`N of M messages within T s`, saying whether a publisher exists). QoS is matched to the publishers, so latched topics work. The payload has nested named fields (`payload.header.stamp.sec`); `timestamp_ns` is `header.stamp` (the publisher's clock, sim time on a simulation), the time in the body of `/clock`, `/tf` and `/rosout` messages (`stamp_source` `payload`), or 0 for other headerless types, with `stamp_source` and `received_ns` (wall clock when the CLI printed it). Arrays are cut at 128 elements by default; `max_array_length` (1..65536, or null for no cut) and `arrays_summary_only` change that, and a cut is listed under `_truncated_fields`. `nan` and `inf` come back as strings. `count` above 50 is capped with a note.
187
187
  - `analyze_bag` (live) parses `ros2 bag info` text for the totals and counts; anomaly detection is mock-only. Per-topic times, rates (`(n - 1) / span`) and `latched` are added when the bag can be read locally (`.db3` with the standard library, `.mcap` with `rosbags`), else rates fall back to count / bag duration (`frequency_basis`). `peek_bag_samples` reads the file itself, through `rosbags`, returns the first `count` messages in recording order (not the last), and is served only by the ROS2 CLI adapter or the mock; its `timestamp_ns` is the message's `header.stamp` (`stamp_source` `header`), the body time of `Clock`, `TFMessage` and `Log` messages (`payload`) or, without either, the bag record time (`stamp_source` `recorded`), and `recorded_ns` is always the bag record time; bags that embed no message definitions (Humble `.db3`) are decoded with the Humble definitions, or the distro the bag records, and `note` says so. Without `ros2`, bag tools return fixtures: check `health_check` for `mode: "mock"` before trusting bag output. `health_check` also reports `sim_clock_published` (live only): true when `/clock` has a publisher, a hint that header stamps may be simulation time.
188
188
  - Calls are serialized per backend (one `ros2` call at a time, one DDS call at a time) and each call has a 45 s wall budget, lock wait included; a call that cannot get its lane fails at once with `busy`. `health_check` and `peek_bag_samples` take no lock.
189
189
  - No streaming or push subscriptions: tools are strictly request/response.
190
190
 
191
- Next: an opt-in probe to tell a hung writer from a healthy one, wider real-bus validation (Fast DDS, RTI, OpenDDS), and DDS Security. Open work is tracked in [issues](https://github.com/yaniswav/TopicForge/issues).
191
+ Not done yet: an opt-in probe to tell a hung writer from a healthy one, wider real-bus validation (RTI, OpenDDS) and DDS Security. Open work is tracked in [issues](https://github.com/yaniswav/TopicForge/issues).
192
192
 
193
193
  ## Telemetry
194
194
 
@@ -204,10 +204,10 @@ When on, each tool call emits one event with exactly six fields:
204
204
 
205
205
  | Field | Example | Notes |
206
206
  | ------------ | --------------- | ----------------------------------------------------------- |
207
- | `tool_name` | `"list_topics"` | One of the sixteen tools, never argument values |
207
+ | `tool_name` | `"list_topics"` | One of the seventeen tools, never argument values |
208
208
  | `latency_ms` | `12.34` | Handler wall-clock duration, 2 decimals |
209
209
  | `mode` | `"mock"` | Mode of the adapter actually serving: `mock` or `live` |
210
- | `version` | `"0.8.1"` | TopicForge server version |
210
+ | `version` | `"0.9.0"` | TopicForge server version |
211
211
  | `session_id` | `"a1b2c3..."` | Random UUID per process, never persisted |
212
212
  | `success` | `true` | Whether the handler returned or raised |
213
213
 
@@ -235,7 +235,7 @@ python -m ruff check src tests
235
235
  python -m pytest
236
236
  ```
237
237
 
238
- Tests run against the mock adapter, the live adapter's pure parsers and the binding-free DDS helpers; they never need a running ROS graph. Tests needing the `cyclonedds` or `fastdds` binding skip themselves when it is absent, and the `integration` marker (real-bus tests) is deselected by default. The `Makefile` (`make check`) uses POSIX shell syntax; on plain PowerShell run the commands above. See [`CONTRIBUTING.md`](CONTRIBUTING.md).
238
+ Tests run against the mock adapter, the live adapter's pure parsers and the binding-free DDS helpers; they never need a running ROS graph. Tests needing the `cyclonedds` binding skip themselves when it is absent, and the `integration` marker (real-bus tests) is deselected by default. The `Makefile` (`make check`) uses POSIX shell syntax; on plain PowerShell run the commands above. See [`CONTRIBUTING.md`](CONTRIBUTING.md).
239
239
 
240
240
  ## Upgrading
241
241
 
@@ -248,13 +248,13 @@ src/topicforge/
248
248
  server/ MCP bootstrap, build_app(settings), prompts and instructions
249
249
  tools/ thin MCPServer handlers, no backend logic
250
250
  services/ input validation, orchestration, adapter factory
251
- adapters/ ros2_live, ros2_mock, dds_cyclone, dds_fast, common/ (binding-free logic)
251
+ adapters/ ros2_live, ros2_mock, dds_cyclone, common/ (binding-free logic)
252
252
  models/ frozen Pydantic schemas, the contract with MCP clients
253
253
  config/ settings and mode resolution
254
254
  telemetry/ opt-in, off by default
255
255
  examples/ mock walkthroughs (*.md) and runnable live DDS examples (dds/)
256
- scripts/ real-bus interop checks
257
- docs/ guides, product plan
256
+ scripts/ real-bus interop checks, ground-truth comparator, agent evaluation
257
+ docs/ guides and reference
258
258
  tests/ pytest suite, mock-only, no ROS2 or DDS required
259
259
  ```
260
260
 
@@ -12,7 +12,7 @@ A read-only MCP (Model Context Protocol) server that lets an AI agent inspect a
12
12
 
13
13
  Every tool also declares itself read-only in the MCP protocol (`readOnlyHint`, `destructiveHint=false`, `idempotentHint`, with an honest `openWorldHint`), and a test fails if any tool lacks it. See [`SECURITY.md`](SECURITY.md#read-only-guarantee-architecture-declaration-proof) for the guarantee and its limits.
14
14
 
15
- It gives the agent sixteen typed tools that return frozen Pydantic schemas (output contract 2: one object per tool, one `note`, units in the field names; see [`docs/CONTRACT.md`](docs/CONTRACT.md)), identical whether the server talks to a real robot or to its built-in mock fixtures. Ask why `nav_planner` gets no scan, and the agent reads the bus, finds the BEST_EFFORT writer facing a RELIABLE reader and names the incompatible policy (see [`examples/02-debug-qos-mismatch.md`](examples/02-debug-qos-mismatch.md)). It is meant for ROS2 developers, robotics ML/CV engineers and teams that cannot accept a write path into a production stack.
15
+ It gives the agent seventeen typed tools that return frozen Pydantic schemas (output contract 2: one object per tool, one `note`, units in the field names; see [`docs/CONTRACT.md`](docs/CONTRACT.md)), identical whether the server talks to a real robot or to its built-in mock fixtures. Ask why `nav_planner` gets no scan, and the agent reads the bus, finds the BEST_EFFORT writer facing a RELIABLE reader and names the incompatible policy (see [`examples/02-debug-qos-mismatch.md`](examples/02-debug-qos-mismatch.md)). It is meant for ROS2 developers, robotics ML/CV engineers and teams that cannot accept a write path into a production stack.
16
16
 
17
17
  For DDS, TopicForge joins a domain as a read-only participant through one open-source binding (Eclipse CycloneDDS from PyPI) and reads the builtin discovery topics that the OMG DDS-RTPS protocol standardizes. Cyclone DDS, Fast DDS and Dust DDS participants have been observed on live buses: the Fast DDS ones on every CI run of the ROS 2 test bench (Humble and Jazzy with `rmw_fastrtps_cpp`) and in the OmniSim validation. RTI Connext, OpenDDS and CoreDX announce themselves through the same standard discovery, but none of them has been observed yet. This covers discovery only: participants, readers, writers and their QoS. See [`docs/dds-interop-matrix.md`](docs/dds-interop-matrix.md).
18
18
 
@@ -42,7 +42,7 @@ The server speaks MCP over stdio and waits for a client, so wire it into one. Fo
42
42
 
43
43
  Then ask it to list the topics or to analyze `/tmp/demo.mcap`. For Claude Code: `claude mcp add topicforge -- topicforge`. If the client cannot launch the server (WSL2 to Windows, a robot over an SSH tunnel), `topicforge --transport streamable-http --port 8765` serves `http://127.0.0.1:8765/mcp` on loopback (another `--host` needs a bearer token in `TOPICFORGE_HTTP_TOKEN`); see [`docs/CLIENTS.md`](docs/CLIENTS.md#local-http-transport).
44
44
 
45
- **Other MCP clients.** Ready-to-paste configs for Claude Desktop, Cursor, VS Code, Windsurf, Cline, Roo Code, Continue, Zed, JetBrains, Codex, Gemini CLI, Goose and more are in [`docs/CLIENTS.md`](docs/CLIENTS.md). One-click: [Add to Cursor](https://cursor.com/install-mcp?name=topicforge&config=eyJjb21tYW5kIjoidXZ4IiwiYXJncyI6WyItLWZyb20iLCJ0b3BpY2ZvcmdlPT0wLjguMSIsInRvcGljZm9yZ2UiXSwiZW52Ijp7IlRPUElDRk9SR0VfTU9ERSI6ImF1dG8iLCJUT1BJQ0ZPUkdFX0REU19CQUNLRU5EIjoiY3ljbG9uZSIsIlRPUElDRk9SR0VfRERTX0RPTUFJTl9JRCI6IjAifX0%3D) | [Install in VS Code](https://vscode.dev/redirect/mcp/install?name=topicforge&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22--from%22%2C%22topicforge%3D%3D0.8.1%22%2C%22topicforge%22%5D%2C%22env%22%3A%7B%22TOPICFORGE_MODE%22%3A%22auto%22%2C%22TOPICFORGE_DDS_BACKEND%22%3A%22cyclone%22%2C%22TOPICFORGE_DDS_DOMAIN_ID%22%3A%220%22%7D%7D).
45
+ **Other MCP clients.** Ready-to-paste configs for Claude Desktop, Cursor, VS Code, Windsurf, Cline, Roo Code, Continue, Zed, JetBrains, Codex, Gemini CLI, Goose and more are in [`docs/CLIENTS.md`](docs/CLIENTS.md). One-click: [Add to Cursor](https://cursor.com/install-mcp?name=topicforge&config=eyJjb21tYW5kIjoidXZ4IiwiYXJncyI6WyItLWZyb20iLCJ0b3BpY2ZvcmdlPT0wLjkuMCIsInRvcGljZm9yZ2UiXSwiZW52Ijp7IlRPUElDRk9SR0VfTU9ERSI6ImF1dG8iLCJUT1BJQ0ZPUkdFX0REU19CQUNLRU5EIjoiY3ljbG9uZSIsIlRPUElDRk9SR0VfRERTX0RPTUFJTl9JRCI6IjAifX0%3D) | [Install in VS Code](https://vscode.dev/redirect/mcp/install?name=topicforge&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22--from%22%2C%22topicforge%3D%3D0.9.0%22%2C%22topicforge%22%5D%2C%22env%22%3A%7B%22TOPICFORGE_MODE%22%3A%22auto%22%2C%22TOPICFORGE_DDS_BACKEND%22%3A%22cyclone%22%2C%22TOPICFORGE_DDS_DOMAIN_ID%22%3A%220%22%7D%7D).
46
46
 
47
47
  Setup for a real ROS2 environment (WSL2, Linux, Docker, native Windows) is in [`docs/TESTING.md`](docs/TESTING.md); recurring monitoring prompts and the privacy contract are in [`docs/TUTORIEL.md`](docs/TUTORIEL.md).
48
48
 
@@ -59,6 +59,7 @@ Every response except `health_check` carries `mode_effective` (`"live"` or `"moc
59
59
  | `get_node_info` | One node's publishers, subscribers, services, actions, parameters (secrets masked) and `use_sim_time`; a node that does not answer is reported, not an error |
60
60
  | `inspect_tf` | The TF tree from `/tf_static` and `/tf`: roots, stale and two-parent frames, disconnected trees, sim-time-safe ages and rates, optional path between two frames |
61
61
  | `graph_diff` | What changed since it last worked: snapshots the ROS 2 graph (or a bag) and diffs it with `previous`, a `snap-N` id or a bag: nodes, topics, types, writer QoS, counts, bag rates |
62
+ | `diagnose` | Is anything wrong: one bounded sweep (nodes, topics, QoS pairs, restarts, TF, `/rosout` errors, a parameter read per node) ranked `critical` / `warning` / `info` with evidence; a check that cannot run is listed, never silent. Holds the ROS lock up to `budget_s` |
62
63
  | `sample_messages` | Peek recent messages on a ROS2 topic (count capped at 50) |
63
64
  | `analyze_bag` | Summarize a `.mcap` / `.db3` recording or `rosbag2_*` directory (via `ros2 bag info`) |
64
65
  | `list_participants` | DDS participants on the domain: vendor, `name` (EntityName QoS, Cyclone) and `hostname` |
@@ -91,9 +92,9 @@ pip install "topicforge[dds]" # alias, kept so older commands
91
92
  TOPICFORGE_DDS_BACKEND=cyclone python -m topicforge
92
93
  ```
93
94
 
94
- `TOPICFORGE_DDS_BACKEND` accepts `mock` (default), `cyclone`, `fast` and `auto` (`fast`, then `cyclone`, then `mock`, whichever binding imports). The default `mock` selects no DDS backend: installing the Cyclone binding is not enough, you must also set `TOPICFORGE_DDS_BACKEND=cyclone`. With `TOPICFORGE_MODE=live` and no backend selected, the DDS tools raise `DDS module is not active: ...` with the actual cause (backend not selected, binding not installed, or binding installed but the adapter failed to start), and `health_check` reports `dds_backend: "none"` plus `dds_inactive_note`. An explicit value is honoured with or without `ros2` on PATH, in any mode except `mock`. If the binding is missing or the participant cannot start, the server logs a warning naming the cause and falls back to the ROS2 CLI alone, or to the mock fixtures. When both `ros2` and a DDS backend are up, a composite adapter routes the eight `ros2` CLI tools (`list_topics`, `get_topic_info`, `list_nodes`, `get_node_info`, `inspect_tf`, `graph_diff`, `sample_messages`, `analyze_bag`) to the CLI and the six DDS tools to the DDS backend; `peek_bag_samples` reads bags without the CLI.
95
+ `TOPICFORGE_DDS_BACKEND` accepts `mock` (default), `cyclone` and `auto` (`cyclone` if the binding imports, else `mock`). The default `mock` selects no DDS backend: installing the Cyclone binding is not enough, you must also set `TOPICFORGE_DDS_BACKEND=cyclone`. With `TOPICFORGE_MODE=live` and no backend selected, the DDS tools raise `DDS module is not active: ...` with the actual cause (backend not selected, binding not installed, or binding installed but the adapter failed to start), and `health_check` reports `dds_backend: "none"` plus `dds_inactive_note`. An explicit value is honoured with or without `ros2` on PATH, in any mode except `mock`. If the binding is missing or the participant cannot start, the server logs a warning naming the cause and falls back to the ROS2 CLI alone, or to the mock fixtures. When both `ros2` and a DDS backend are up, a composite adapter routes the nine `ros2` CLI tools (`list_topics`, `get_topic_info`, `list_nodes`, `get_node_info`, `inspect_tf`, `graph_diff`, `diagnose`, `sample_messages`, `analyze_bag`) to the CLI and the six DDS tools to the DDS backend; `peek_bag_samples` goes to the ROS half but runs no `ros2` process.
95
96
 
96
- A Fast DDS adapter exists but has never run against a bus, and its `fastdds` Python binding is not on PyPI: build it from eProsima's sources and install it next to TopicForge. There is no `[dds-fast]` extra. Any other value (`rti`, `opensplice`, `coredx`, `intercom`, `opendds`, `dust`) is rejected at startup with an error that lists the valid ones; Cyclone already sees those vendors' participants through standard discovery. Full backend selection, the routing table and the QoS mismatch scenario are in [`docs/DDS_QUICKSTART.md`](docs/DDS_QUICKSTART.md); error messages are in [`docs/TROUBLESHOOTING.md`](docs/TROUBLESHOOTING.md).
97
+ Fast DDS participants (ROS 2 nodes on `rmw_fastrtps_cpp`) need no backend of their own: the Cyclone backend observes them through standard RTPS discovery. Since 0.9.0 `TOPICFORGE_DDS_BACKEND=fast` is refused, like `rti`, `opensplice`, `coredx`, `intercom`, `opendds` and `dust`: startup stops with an error that lists the valid values (see [`docs/TROUBLESHOOTING.md`](docs/TROUBLESHOOTING.md)). Full backend selection, the routing table and the QoS mismatch scenario are in [`docs/DDS_QUICKSTART.md`](docs/DDS_QUICKSTART.md).
97
98
 
98
99
  ## Configuration reference
99
100
 
@@ -103,7 +104,7 @@ A Fast DDS adapter exists but has never run against a bus, and its `fastdds` Pyt
103
104
  | `TOPICFORGE_LOG_LEVEL` | `INFO` | `DEBUG`, `INFO`, `WARNING`, `ERROR` |
104
105
  | `TOPICFORGE_ROS2_BIN` | `ros2` | Name or path of the ROS2 CLI binary |
105
106
  | `TOPICFORGE_TELEMETRY` | `off` | Opt-in anonymous telemetry; an unrecognized value aborts startup. See [Telemetry](#telemetry) |
106
- | `TOPICFORGE_DDS_BACKEND` | `mock` | `mock` (no DDS backend), `cyclone`, `fast`, `auto`; anything else is rejected |
107
+ | `TOPICFORGE_DDS_BACKEND` | `mock` | `mock` (no DDS backend), `cyclone`, `auto`; anything else is rejected |
107
108
  | `TOPICFORGE_DDS_DOMAIN_ID` | `0` | DDS domain observed (0..232). Joined at startup; changing it needs a restart |
108
109
  | `TOPICFORGE_MAX_SAMPLE_BYTES` | `1048576` | Size cap for one sampled message (1 KiB..64 MiB); a call returns at most 4 times that. Over-cap messages are dropped with a note |
109
110
 
@@ -115,19 +116,18 @@ Samples with comments are in [`.env.example`](.env.example). Any invalid value s
115
116
 
116
117
  External validation against a simulated robot's ground truth (OmniSim): [docs/VALIDATION.md](docs/VALIDATION.md).
117
118
 
118
- - DDS validation is partial. The Cyclone adapter has run against real buses with Cyclone, Fast DDS and Dust DDS participants, on Windows and in CI on Ubuntu and Windows (`.github/workflows/demo.yml`, `.github/workflows/ros2-live.yml`). The Fast DDS adapter (TopicForge joining through the `fastdds` binding) has never run against a bus, and no RTI, OpenDDS, CoreDX or OpenSplice participant has been observed by this project. The multi-vendor claim rests on the RTPS protocol guarantee, not on a recorded cross-vendor run.
119
+ - DDS validation is partial. The Cyclone adapter has run against real buses with Cyclone, Fast DDS and Dust DDS participants, on Windows and in CI on Ubuntu and Windows (`.github/workflows/demo.yml`, `.github/workflows/ros2-live.yml`). No RTI, OpenDDS, CoreDX or OpenSplice participant has been observed by this project. The multi-vendor claim rests on the RTPS protocol guarantee, not on a recorded cross-vendor run.
119
120
  - User-topic payloads are not decoded. `peek_dds_samples` on a user topic returns count 0 and a note that the topic is announced on the bus; no traffic is read. `topic_metrics` therefore has data only for the builtin discovery topics and says so in its `status`. It is a discovery-layer probe, not a publish-rate monitor.
120
121
  - Liveliness at runtime is not observed. A writer that is alive but silent (a hung process whose lease is still renewed) looks healthy, because TopicForge reads discovery, not data. An opt-in data probe is planned. A crash and a clean leave cannot be told apart, and `lost_ns` is an upper bound of the death.
121
122
  - Cyclone vendor ids: participants that do not follow the RTPS vendor-id convention in their GUID prefix (Dust DDS, and RTI by default) are reported with vendor `unknown`.
122
123
  - Single domain: the server observes the domain it joined at startup; changing it needs a restart.
123
124
  - DDS Security is not handled. A participant without credentials sees an empty secure bus. `detect_qos_mismatches` checks Partition, type name, Reliability, Durability, Deadline, Liveliness, LatencyBudget, Ownership (kind), DestinationOrder and DataRepresentation (History as a risk); Presentation, XTypes assignability and runtime behavior are not checked, and the result lists them in `policies_unchecked`. It returns a `MismatchScan` envelope: read `reports` for the mismatches.
124
- - Fast DDS serves no `list_endpoints`.
125
125
  - `sample_messages` (live) streams `ros2 topic echo` until `count` messages arrive or `timeout_s` (1..40, default 10, for the whole call) runs out, and returns what arrived with a `note` (`N of M messages within T s`, saying whether a publisher exists). QoS is matched to the publishers, so latched topics work. The payload has nested named fields (`payload.header.stamp.sec`); `timestamp_ns` is `header.stamp` (the publisher's clock, sim time on a simulation), the time in the body of `/clock`, `/tf` and `/rosout` messages (`stamp_source` `payload`), or 0 for other headerless types, with `stamp_source` and `received_ns` (wall clock when the CLI printed it). Arrays are cut at 128 elements by default; `max_array_length` (1..65536, or null for no cut) and `arrays_summary_only` change that, and a cut is listed under `_truncated_fields`. `nan` and `inf` come back as strings. `count` above 50 is capped with a note.
126
126
  - `analyze_bag` (live) parses `ros2 bag info` text for the totals and counts; anomaly detection is mock-only. Per-topic times, rates (`(n - 1) / span`) and `latched` are added when the bag can be read locally (`.db3` with the standard library, `.mcap` with `rosbags`), else rates fall back to count / bag duration (`frequency_basis`). `peek_bag_samples` reads the file itself, through `rosbags`, returns the first `count` messages in recording order (not the last), and is served only by the ROS2 CLI adapter or the mock; its `timestamp_ns` is the message's `header.stamp` (`stamp_source` `header`), the body time of `Clock`, `TFMessage` and `Log` messages (`payload`) or, without either, the bag record time (`stamp_source` `recorded`), and `recorded_ns` is always the bag record time; bags that embed no message definitions (Humble `.db3`) are decoded with the Humble definitions, or the distro the bag records, and `note` says so. Without `ros2`, bag tools return fixtures: check `health_check` for `mode: "mock"` before trusting bag output. `health_check` also reports `sim_clock_published` (live only): true when `/clock` has a publisher, a hint that header stamps may be simulation time.
127
127
  - Calls are serialized per backend (one `ros2` call at a time, one DDS call at a time) and each call has a 45 s wall budget, lock wait included; a call that cannot get its lane fails at once with `busy`. `health_check` and `peek_bag_samples` take no lock.
128
128
  - No streaming or push subscriptions: tools are strictly request/response.
129
129
 
130
- Next: an opt-in probe to tell a hung writer from a healthy one, wider real-bus validation (Fast DDS, RTI, OpenDDS), and DDS Security. Open work is tracked in [issues](https://github.com/yaniswav/TopicForge/issues).
130
+ Not done yet: an opt-in probe to tell a hung writer from a healthy one, wider real-bus validation (RTI, OpenDDS) and DDS Security. Open work is tracked in [issues](https://github.com/yaniswav/TopicForge/issues).
131
131
 
132
132
  ## Telemetry
133
133
 
@@ -143,10 +143,10 @@ When on, each tool call emits one event with exactly six fields:
143
143
 
144
144
  | Field | Example | Notes |
145
145
  | ------------ | --------------- | ----------------------------------------------------------- |
146
- | `tool_name` | `"list_topics"` | One of the sixteen tools, never argument values |
146
+ | `tool_name` | `"list_topics"` | One of the seventeen tools, never argument values |
147
147
  | `latency_ms` | `12.34` | Handler wall-clock duration, 2 decimals |
148
148
  | `mode` | `"mock"` | Mode of the adapter actually serving: `mock` or `live` |
149
- | `version` | `"0.8.1"` | TopicForge server version |
149
+ | `version` | `"0.9.0"` | TopicForge server version |
150
150
  | `session_id` | `"a1b2c3..."` | Random UUID per process, never persisted |
151
151
  | `success` | `true` | Whether the handler returned or raised |
152
152
 
@@ -174,7 +174,7 @@ python -m ruff check src tests
174
174
  python -m pytest
175
175
  ```
176
176
 
177
- Tests run against the mock adapter, the live adapter's pure parsers and the binding-free DDS helpers; they never need a running ROS graph. Tests needing the `cyclonedds` or `fastdds` binding skip themselves when it is absent, and the `integration` marker (real-bus tests) is deselected by default. The `Makefile` (`make check`) uses POSIX shell syntax; on plain PowerShell run the commands above. See [`CONTRIBUTING.md`](CONTRIBUTING.md).
177
+ Tests run against the mock adapter, the live adapter's pure parsers and the binding-free DDS helpers; they never need a running ROS graph. Tests needing the `cyclonedds` binding skip themselves when it is absent, and the `integration` marker (real-bus tests) is deselected by default. The `Makefile` (`make check`) uses POSIX shell syntax; on plain PowerShell run the commands above. See [`CONTRIBUTING.md`](CONTRIBUTING.md).
178
178
 
179
179
  ## Upgrading
180
180
 
@@ -187,13 +187,13 @@ src/topicforge/
187
187
  server/ MCP bootstrap, build_app(settings), prompts and instructions
188
188
  tools/ thin MCPServer handlers, no backend logic
189
189
  services/ input validation, orchestration, adapter factory
190
- adapters/ ros2_live, ros2_mock, dds_cyclone, dds_fast, common/ (binding-free logic)
190
+ adapters/ ros2_live, ros2_mock, dds_cyclone, common/ (binding-free logic)
191
191
  models/ frozen Pydantic schemas, the contract with MCP clients
192
192
  config/ settings and mode resolution
193
193
  telemetry/ opt-in, off by default
194
194
  examples/ mock walkthroughs (*.md) and runnable live DDS examples (dds/)
195
- scripts/ real-bus interop checks
196
- docs/ guides, product plan
195
+ scripts/ real-bus interop checks, ground-truth comparator, agent evaluation
196
+ docs/ guides and reference
197
197
  tests/ pytest suite, mock-only, no ROS2 or DDS required
198
198
  ```
199
199
 
@@ -6,7 +6,7 @@ TopicForge (ROS 2 / DDS) is a local, read-only MCP server that speaks stdio.
6
6
  Every client below launches it as a child process with the same command:
7
7
 
8
8
  ```
9
- uvx --from "topicforge==0.8.1" topicforge
9
+ uvx --from "topicforge==0.9.0" topicforge
10
10
  ```
11
11
 
12
12
  and the same three environment variables:
@@ -21,7 +21,7 @@ The registry name is `io.github.yaniswav/topicforge`. Where a client asks for a
21
21
  display name, use **TopicForge (ROS 2 / DDS)**: an unrelated SEO product is also
22
22
  called TopicForge in the MCP registries.
23
23
 
24
- The version pin (`==0.8.1`) is moved on every release together with the other
24
+ The version pin (`==0.9.0`) is moved on every release together with the other
25
25
  version strings; a test keeps them in sync.
26
26
 
27
27
  ## Before you start
@@ -44,7 +44,7 @@ version strings; a test keeps them in sync.
44
44
  `"--from"` in the args. To build the binding yourself, install the Cyclone C library and
45
45
  run `pip install "topicforge[dds]"`. The `[dds]` and `[dds-cyclone]` extras still work
46
46
  as aliases, so older commands keep installing.
47
- - **pip alternative.** `pip install "topicforge==0.8.1"`, then use
47
+ - **pip alternative.** `pip install "topicforge==0.9.0"`, then use
48
48
  `"command": "topicforge"` (or `"command": "python", "args": ["-m", "topicforge"]`)
49
49
  with no `args` for uvx. Use the absolute path of the binary if the client does
50
50
  not see your venv.
@@ -72,7 +72,7 @@ say where the file lives and what differs.
72
72
  "mcpServers": {
73
73
  "topicforge": {
74
74
  "command": "uvx",
75
- "args": ["--from", "topicforge==0.8.1", "topicforge"],
75
+ "args": ["--from", "topicforge==0.9.0", "topicforge"],
76
76
  "env": {
77
77
  "TOPICFORGE_MODE": "auto",
78
78
  "TOPICFORGE_DDS_BACKEND": "cyclone",
@@ -95,7 +95,7 @@ claude plugin install topicforge@topicforge
95
95
  Or just the server:
96
96
 
97
97
  ```
98
- claude mcp add topicforge --env TOPICFORGE_MODE=auto --env TOPICFORGE_DDS_BACKEND=cyclone --env TOPICFORGE_DDS_DOMAIN_ID=0 -- uvx --from "topicforge==0.8.1" topicforge
98
+ claude mcp add topicforge --env TOPICFORGE_MODE=auto --env TOPICFORGE_DDS_BACKEND=cyclone --env TOPICFORGE_DDS_DOMAIN_ID=0 -- uvx --from "topicforge==0.9.0" topicforge
99
99
  ```
100
100
 
101
101
  ## Claude Desktop
@@ -112,9 +112,9 @@ the common JSON shape above and restart the app completely. On Windows, if
112
112
 
113
113
  The one-click links in this page and in the README are https redirects, because GitHub and PyPI strip `cursor://` and `vscode:` links. Only the raw schemes are documented by Cursor and VS Code; the https forms (`cursor.com/install-mcp`, `vscode.dev/redirect/mcp/install`) were observed to redirect correctly on 2026-10-06 but are not in the vendors' docs (unverified, last checked 2026-10-06). The raw links are given as a fallback.
114
114
 
115
- One click: [Add TopicForge to Cursor](https://cursor.com/install-mcp?name=topicforge&config=eyJjb21tYW5kIjoidXZ4IiwiYXJncyI6WyItLWZyb20iLCJ0b3BpY2ZvcmdlPT0wLjguMSIsInRvcGljZm9yZ2UiXSwiZW52Ijp7IlRPUElDRk9SR0VfTU9ERSI6ImF1dG8iLCJUT1BJQ0ZPUkdFX0REU19CQUNLRU5EIjoiY3ljbG9uZSIsIlRPUElDRk9SR0VfRERTX0RPTUFJTl9JRCI6IjAifX0%3D)
115
+ One click: [Add TopicForge to Cursor](https://cursor.com/install-mcp?name=topicforge&config=eyJjb21tYW5kIjoidXZ4IiwiYXJncyI6WyItLWZyb20iLCJ0b3BpY2ZvcmdlPT0wLjkuMCIsInRvcGljZm9yZ2UiXSwiZW52Ijp7IlRPUElDRk9SR0VfTU9ERSI6ImF1dG8iLCJUT1BJQ0ZPUkdFX0REU19CQUNLRU5EIjoiY3ljbG9uZSIsIlRPUElDRk9SR0VfRERTX0RPTUFJTl9JRCI6IjAifX0%3D)
116
116
 
117
- If that page does not open Cursor, use the raw deeplink: `cursor://anysphere.cursor-deeplink/mcp/install?name=topicforge&config=eyJjb21tYW5kIjoidXZ4IiwiYXJncyI6WyItLWZyb20iLCJ0b3BpY2ZvcmdlPT0wLjguMSIsInRvcGljZm9yZ2UiXSwiZW52Ijp7IlRPUElDRk9SR0VfTU9ERSI6ImF1dG8iLCJUT1BJQ0ZPUkdFX0REU19CQUNLRU5EIjoiY3ljbG9uZSIsIlRPUElDRk9SR0VfRERTX0RPTUFJTl9JRCI6IjAifX0%3D`
117
+ If that page does not open Cursor, use the raw deeplink: `cursor://anysphere.cursor-deeplink/mcp/install?name=topicforge&config=eyJjb21tYW5kIjoidXZ4IiwiYXJncyI6WyItLWZyb20iLCJ0b3BpY2ZvcmdlPT0wLjkuMCIsInRvcGljZm9yZ2UiXSwiZW52Ijp7IlRPUElDRk9SR0VfTU9ERSI6ImF1dG8iLCJUT1BJQ0ZPUkdFX0REU19CQUNLRU5EIjoiY3ljbG9uZSIsIlRPUElDRk9SR0VfRERTX0RPTUFJTl9JRCI6IjAifX0%3D`
118
118
 
119
119
  Or paste the common JSON shape into `~/.cursor/mcp.json` (all projects) or
120
120
  `.cursor/mcp.json` (one project).
@@ -125,9 +125,9 @@ The `plugin/` folder of this repository is also an [Agent Plugins](https://agent
125
125
 
126
126
  ## VS Code / GitHub Copilot
127
127
 
128
- One click: [Install in VS Code](https://vscode.dev/redirect/mcp/install?name=topicforge&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22--from%22%2C%22topicforge%3D%3D0.8.1%22%2C%22topicforge%22%5D%2C%22env%22%3A%7B%22TOPICFORGE_MODE%22%3A%22auto%22%2C%22TOPICFORGE_DDS_BACKEND%22%3A%22cyclone%22%2C%22TOPICFORGE_DDS_DOMAIN_ID%22%3A%220%22%7D%7D)
128
+ One click: [Install in VS Code](https://vscode.dev/redirect/mcp/install?name=topicforge&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22--from%22%2C%22topicforge%3D%3D0.9.0%22%2C%22topicforge%22%5D%2C%22env%22%3A%7B%22TOPICFORGE_MODE%22%3A%22auto%22%2C%22TOPICFORGE_DDS_BACKEND%22%3A%22cyclone%22%2C%22TOPICFORGE_DDS_DOMAIN_ID%22%3A%220%22%7D%7D)
129
129
 
130
- Fallback, the raw URL handler: `vscode:mcp/install?%7B%22name%22%3A%22topicforge%22%2C%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22--from%22%2C%22topicforge%3D%3D0.8.1%22%2C%22topicforge%22%5D%2C%22env%22%3A%7B%22TOPICFORGE_MODE%22%3A%22auto%22%2C%22TOPICFORGE_DDS_BACKEND%22%3A%22cyclone%22%2C%22TOPICFORGE_DDS_DOMAIN_ID%22%3A%220%22%7D%7D`
130
+ Fallback, the raw URL handler: `vscode:mcp/install?%7B%22name%22%3A%22topicforge%22%2C%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22--from%22%2C%22topicforge%3D%3D0.9.0%22%2C%22topicforge%22%5D%2C%22env%22%3A%7B%22TOPICFORGE_MODE%22%3A%22auto%22%2C%22TOPICFORGE_DDS_BACKEND%22%3A%22cyclone%22%2C%22TOPICFORGE_DDS_DOMAIN_ID%22%3A%220%22%7D%7D`
131
131
 
132
132
  Or put this in `.vscode/mcp.json` (the key is `servers`, not `mcpServers`), or run
133
133
  "MCP: Add Server" from the Command Palette:
@@ -138,7 +138,7 @@ Or put this in `.vscode/mcp.json` (the key is `servers`, not `mcpServers`), or r
138
138
  "topicforge": {
139
139
  "type": "stdio",
140
140
  "command": "uvx",
141
- "args": ["--from", "topicforge==0.8.1", "topicforge"],
141
+ "args": ["--from", "topicforge==0.9.0", "topicforge"],
142
142
  "env": {
143
143
  "TOPICFORGE_MODE": "auto",
144
144
  "TOPICFORGE_DDS_BACKEND": "cyclone",
@@ -167,7 +167,7 @@ Servers", and paste the common JSON shape (the file is
167
167
  Project file `.roo/mcp.json` (takes precedence) or the global `mcp_settings.json`
168
168
  opened from the MCP view. Paste the common JSON shape. Roo's docs say Windows
169
169
  needs a `cmd /c` wrapper for `npx`; for `uvx`, try it directly first and fall back
170
- to `"command": "cmd", "args": ["/c", "uvx", "--from", "topicforge==0.8.1",
170
+ to `"command": "cmd", "args": ["/c", "uvx", "--from", "topicforge==0.9.0",
171
171
  "topicforge"]` if it fails to start.
172
172
 
173
173
  ## Continue
@@ -184,7 +184,7 @@ mcpServers:
184
184
  command: uvx
185
185
  args:
186
186
  - "--from"
187
- - "topicforge==0.8.1"
187
+ - "topicforge==0.9.0"
188
188
  - "topicforge"
189
189
  env:
190
190
  TOPICFORGE_MODE: auto
@@ -201,7 +201,7 @@ In `settings.json`, under `context_servers`:
201
201
  "context_servers": {
202
202
  "topicforge": {
203
203
  "command": "uvx",
204
- "args": ["--from", "topicforge==0.8.1", "topicforge"],
204
+ "args": ["--from", "topicforge==0.9.0", "topicforge"],
205
205
  "env": {
206
206
  "TOPICFORGE_MODE": "auto",
207
207
  "TOPICFORGE_DDS_BACKEND": "cyclone",
@@ -227,7 +227,7 @@ run with the dds extra can exceed, so set it to 60 s:
227
227
  ```toml
228
228
  [mcp_servers.topicforge]
229
229
  command = "uvx"
230
- args = ["--from", "topicforge==0.8.1", "topicforge"]
230
+ args = ["--from", "topicforge==0.9.0", "topicforge"]
231
231
  startup_timeout_sec = 60
232
232
 
233
233
  [mcp_servers.topicforge.env]
@@ -258,7 +258,7 @@ extensions:
258
258
  name: TopicForge (ROS 2 / DDS)
259
259
  type: stdio
260
260
  cmd: uvx
261
- args: [--from, "topicforge==0.8.1", topicforge]
261
+ args: [--from, "topicforge==0.9.0", topicforge]
262
262
  enabled: true
263
263
  envs: { "TOPICFORGE_MODE": "auto", "TOPICFORGE_DDS_BACKEND": "cyclone", "TOPICFORGE_DDS_DOMAIN_ID": "0" }
264
264
  timeout: 300
@@ -269,7 +269,7 @@ extensions:
269
269
  Program tab > Install > Edit `mcp.json` (same notation as Cursor), then paste the
270
270
  common JSON shape. LM Studio's docs only show remote examples; local command
271
271
  servers follow the Cursor notation but are not documented explicitly. Small
272
- local models may struggle with sixteen tools.
272
+ local models may struggle with seventeen tools.
273
273
 
274
274
  ## Amazon Q Developer / Kiro
275
275
 
@@ -367,7 +367,7 @@ when the question matches.
367
367
 
368
368
  ## Troubleshooting
369
369
 
370
- - Run the command by hand first: `uvx --from "topicforge==0.8.1" topicforge --version`.
370
+ - Run the command by hand first: `uvx --from "topicforge==0.9.0" topicforge --version`.
371
371
  - Server starts but sees no DDS participants: check the domain id, and that the
372
372
  client machine is on the same network as the robot (DDS discovery uses
373
373
  multicast UDP). Ask the assistant to call `health_check`.
@@ -523,7 +523,7 @@ moves to the envelope of the listing.
523
523
  ### 7.3 Dead fields removed from `TopicInfo`
524
524
 
525
525
  `reader_count`, `writer_count` and `qos_profile` are removed. Checked by grep over `src/`:
526
- no adapter (`ros2_live`, `ros2_mock`, `dds_cyclone`, `dds_fast`, `composite`) and no
526
+ no adapter (`ros2_live`, `ros2_mock`, `dds_cyclone`, `dds_fast` (removed in 0.9.0), `composite`) and no
527
527
  service ever assigns them, so they have always been `null`. Not to be confused with
528
528
  `TopicSummary.writer_count` and `TopicSummary.reader_count` in `list_endpoints`, which are
529
529
  filled and stay.
@@ -579,7 +579,7 @@ payloads are `_decode_status`, `_decode_note`, `_truncated_fields`, `_raw_text`,
579
579
  overloaded: this is additive.
580
580
  - `MessageSample.stamp_source` becomes non-nullable, with five values (2.2). It is set
581
581
  explicitly in `adapters/dds_cyclone/adapter.py` (lines 165 and 442),
582
- `adapters/dds_fast/adapter.py` (line 314), `adapters/common/dds_helpers.py`
582
+ `adapters/dds_fast/adapter.py` (line 314; adapter removed in 0.9.0), `adapters/common/dds_helpers.py`
583
583
  (line 135) and the DDS fixtures of the mock adapter.
584
584
 
585
585
  ### 7.7 Decided by audit
@@ -1286,3 +1286,104 @@ Additive. `tools/list` grows by 467 bytes, to 97,315 (measured like `test_tool_l
1286
1286
  `/robot_description` (section 9.15).
1287
1287
  - HTTP transport: `TOPICFORGE_HTTP_TOKEN` and `--host` (SECURITY.md, docs/CLIENTS.md). No output
1288
1288
  change.
1289
+
1290
+ ### 9.19 Node names on QoS pairs (0.9.0, fix)
1291
+
1292
+ No shape change. On a live DDS backend, `detect_qos_mismatches` fills `reader_node` and
1293
+ `writer_node` from the same gid -> node map as `list_endpoints` (section 9.16). In 0.8.1 the QoS
1294
+ scan built its endpoints without that map, so both fields were `null` on every pair even when
1295
+ `list_endpoints` named the nodes. The mock already filled them, which is why the tests did not
1296
+ see it. Comparator: section `visibility`, row "QoS pairs name the writer node" on
1297
+ `06_detect_qos_mismatches`.
1298
+
1299
+ ### 9.20 `tools/list` size: lane notes, field descriptions, ceiling (0.9.0)
1300
+
1301
+ Descriptions only, no shape change. `tools/list` goes from 97,315 to 95,391 bytes before
1302
+ `diagnose` (measured like `test_tool_listing_stays_small`).
1303
+
1304
+ - **Lane notes.** The `**Duration**` sentence of section 9.13 is shorter:
1305
+ ``**Duration**: ROS lock, one call at a time, ends within 45 s; `busy` if held.`` (DDS lock for
1306
+ the DDS tools). Same facts; `test_tool_descriptions.py` still checks the lock, `45 s` and `busy`.
1307
+ - **Read-only phrases.** "Read-only; no side effects." and its variants are gone from the ROS 2
1308
+ tool descriptions: every tool declares `readOnlyHint` (section 2). "**Read-only by
1309
+ architecture**" stays on the DDS tools, which join the bus.
1310
+ - **Output field descriptions** are at most 120 characters, rewritten by hand with the same facts
1311
+ (`test_output_field_descriptions_stay_short`). Two lost a pointer only: `vendor` no longer names
1312
+ `vendor_source`, and `seen_count` no longer names `restarts_by_name`.
1313
+ - **Ceiling.** The test ceiling moves from 100,000 to 105,000 bytes, and the comparator's
1314
+ `tools/list bytes` row with it. No known client breaks in that range; the clients that truncate
1315
+ do it near 40,000 characters, a line crossed since 0.7.0.
1316
+
1317
+ ### 9.21 diagnose (tool 17, 0.9.0)
1318
+
1319
+ The seventeenth tool served (the slot numbered 18 in the plan; 16 stays reserved for `get_image`).
1320
+ Additive: no existing tool changes shape. `tools/list` goes from 95,391 to 99,658 bytes (`diagnose`
1321
+ is 4,267: description 775, input schema 449, output schema 2,842), under the 105,000 test ceiling
1322
+ of section 9.20.
1323
+
1324
+ **Call.** `diagnose(scope="all", budget_s=30, node=null)`. `scope` is `all`, `graph` (nodes,
1325
+ topics, parameter reads), `tf`, `dds` or `logs`; `budget_s` is 10..40; `node` is read first. Only
1326
+ invalid input raises (an `AdapterError`, so `isError: true`); a failing check never does.
1327
+
1328
+ **Lane and budget.** ROS lane. The call holds the ROS lock for up to `budget_s`: other ROS tools
1329
+ answer `busy` meanwhile, `health_check` does not. The internal deadline is
1330
+ `min(now + budget_s, guard deadline - 2 s)`; a check that no longer fits is listed in
1331
+ `checks_skipped` with reason `budget`. The tf window shrinks to what is left (an `info` says so).
1332
+
1333
+ **Sources.** Existing services only, no adapter method added: `list_nodes`, `list_topics`,
1334
+ `get_node_info`, `inspect_tf` (`window_s` 8, `stale_after_s` 2), `sample_messages` (`/rosout`, 50,
1335
+ `min_log_level` `error`, `timeout_s` 5: only lines logged while it listens), `detect_qos_mismatches`, `participant_events` (300 s),
1336
+ `list_participants` and the health report. On the DDS side it never calls `peek_dds_samples` or
1337
+ `topic_metrics`: the three DDS calls it makes are answered by the discovery caches
1338
+ (`tests/test_diagnose.py` records the adapter methods called and fails on either).
1339
+
1340
+ **Checks, in execution order, and the severity table.** A `critical` exists only for a row below.
1341
+
1342
+ | Check | critical | warning | info |
1343
+ |---|---|---|---|
1344
+ | `environment` | - | - | mock mode; SROS2 enabled. A missing backend is a `checks_skipped` entry |
1345
+ | `nodes` | - | a duplicated name; no node on a live graph | lifecycle nodes (state not read) |
1346
+ | `topics` | - | - | subscribers without a publisher, one finding (normal for command inputs such as `/cmd_vel`); publishers without a subscriber, one finding (`/rosout` and `/parameter_events` left out) |
1347
+ | `dds` | an `incompatible` QoS pair; a pair that never matches (partition, type name); a node with 3 or more lives in the window | a `risky` pair; a near-homonym topic hint; a node with 2 lives | participants that left; no endpoint visible |
1348
+ | `tf` | - | `stale`, `two_parents`, `cycle`, `static_and_dynamic`; a URDF link with no frame | several roots; `urdf_note`; a window cut by the budget |
1349
+ | `logs` | - | one per node that logged an `error` or `fatal` line (the newest line quoted) | no such line |
1350
+ | `responsiveness` | the node named in `node` did not answer a parameter read in 6 s | another node did not answer in 3 s (the text says to confirm with `get_node_info(node, timeout_s=8)`) | nodes the budget did not reach, named |
1351
+
1352
+ `responsiveness` runs last, as the budget sink: nodes in the order named, lifecycle, then by name
1353
+ (`_ros2cli_*` left out), 3 s each while the budget lasts. A writer without a reader is never above
1354
+ `info`, and `ros_discovery_info` pairs stay out of the QoS scan.
1355
+
1356
+ **Skips.** An `AdapterError` inside a check, or a backend the health report says is absent (no
1357
+ `ros2` CLI for the ROS checks, no DDS backend for `dds`), becomes a `checks_skipped` entry
1358
+ `{check, reason}` carrying the message. A node whose read raises (the named node is not on the
1359
+ graph) is listed the same way, with the node in `reason`, and the other nodes are still read.
1360
+
1361
+ **Output.** `Diagnosis`: `findings` sorted by severity (`critical`, `warning`, `info`), then
1362
+ `check`, then `subject`, capped at 50 with `findings_total`; `critical_count`, `warning_count` and
1363
+ `info_count` cover all findings; `checks_run`, `checks_skipped`, `budget_s`, `elapsed_s`,
1364
+ `verdict`, `mode_effective`, `note`. A `Finding` has `severity`, `check`, `subject` (topic, node or
1365
+ frame), `text`, `evidence` (`{tool, field, value}`, `value` cut at 200 characters) and
1366
+ `suggested_fixes` (the `SuggestedFix` of section 3.1, copied from `detect_qos_mismatches`, empty
1367
+ elsewhere; the forbidden-verb test covers the copy).
1368
+
1369
+ **Never healthy with a skip.** The verdict is one line and never says an all-clear while a check
1370
+ was skipped or the scope left checks out: it starts `Incomplete sweep:` and names what did not
1371
+ run. Only a full sweep with no skip and no critical or warning finding says `no critical or
1372
+ warning finding in the N checks run; data flow itself is not observed`. The word "healthy" is
1373
+ not used: discovery and `/rosout` show declared state, not data flow. On a live ROS 2-only setup
1374
+ (no DDS backend) the `dds` check is always skipped, so the verdict is never the clean line.
1375
+
1376
+ **Mock.** The fixtures give one `critical` (the RELIABLE reader against the BEST_EFFORT writer on
1377
+ `/dds/qos_mismatch`, with its fixes), warnings for the stale `map -> odom`, the URDF link
1378
+ `gripper_link` and one per node with an error on `/rosout`, and three `info`; `checks_skipped` is
1379
+ empty and `elapsed_s` is under 1 s (`test_the_mock_diagnosis_is_derived_from_the_fixtures`).
1380
+
1381
+ **Bench.** `calls_diagnose.json` (between `calls_nav_primitives.json` and `calls_graph_diff.json`,
1382
+ bench only): `50_diagnose` (`budget_s` 40) and `51_diagnose_node` (`node` `/bench_blocked`). The
1383
+ truth is the exact set of critical and warning findings the bench seeds (`stale` `map -> odom`,
1384
+ `tool` with two parents, `arm_link`, the errors of `/bench_robot`, the blocked node: a `warning`
1385
+ in `50`, or an `info` that names it if the budget did not reach it; a `critical` in `51`). Any
1386
+ other critical or warning is a false alarm and fails the cell. Also required: `checks_skipped`
1387
+ empty, `elapsed_s` within `budget_s`, counts that match the findings, a verdict that is not the
1388
+ clean line. Comparator section: `diagnose`.
1389
+