topicforge 0.5.0__tar.gz → 0.5.2__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 (107) hide show
  1. {topicforge-0.5.0 → topicforge-0.5.2}/CHANGELOG.md +195 -1
  2. {topicforge-0.5.0 → topicforge-0.5.2}/PKG-INFO +8 -4
  3. {topicforge-0.5.0 → topicforge-0.5.2}/README.md +3 -1
  4. {topicforge-0.5.0 → topicforge-0.5.2}/docs/DDS_QUICKSTART.md +1 -1
  5. topicforge-0.5.2/docs/TUTORIEL.md +204 -0
  6. {topicforge-0.5.0 → topicforge-0.5.2}/docs/product-plan.md +4 -4
  7. {topicforge-0.5.0 → topicforge-0.5.2}/pyproject.toml +35 -2
  8. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/__init__.py +1 -1
  9. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/adapters/common/__init__.py +30 -0
  10. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/adapters/common/cdr_decoder.py +22 -5
  11. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/adapters/common/dds_helpers.py +18 -0
  12. topicforge-0.5.2/src/topicforge/adapters/common/dds_introspection.py +184 -0
  13. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/adapters/common/lifecycle.py +33 -4
  14. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/adapters/common/metrics_buffer.py +70 -20
  15. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/adapters/common/qos_analyzer.py +21 -9
  16. topicforge-0.5.2/src/topicforge/adapters/common/qos_endpoints.py +95 -0
  17. topicforge-0.5.2/src/topicforge/adapters/common/qos_normalize.py +178 -0
  18. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/adapters/common/xtypes.py +8 -5
  19. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/adapters/dds_cyclone/adapter.py +92 -207
  20. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/adapters/dds_dust/adapter.py +2 -2
  21. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/adapters/dds_fast/adapter.py +82 -199
  22. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/adapters/dds_opendds/adapter.py +16 -10
  23. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/adapters/ros2_mock/adapter.py +3 -4
  24. topicforge-0.5.2/src/topicforge/constants.py +28 -0
  25. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/services/bag_service.py +3 -13
  26. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/services/health.py +1 -1
  27. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/services/inspector.py +2 -6
  28. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/tools/handlers.py +8 -5
  29. {topicforge-0.5.0 → topicforge-0.5.2}/tests/integration/test_scenarios_schema.py +2 -1
  30. {topicforge-0.5.0 → topicforge-0.5.2}/tests/test_bag_service.py +5 -2
  31. {topicforge-0.5.0 → topicforge-0.5.2}/tests/test_cdr_decoder.py +34 -0
  32. {topicforge-0.5.0 → topicforge-0.5.2}/tests/test_dds_helpers.py +41 -0
  33. topicforge-0.5.2/tests/test_dds_introspection.py +215 -0
  34. topicforge-0.5.2/tests/test_dds_qos_normalization.py +286 -0
  35. {topicforge-0.5.0 → topicforge-0.5.2}/tests/test_health.py +1 -1
  36. {topicforge-0.5.0 → topicforge-0.5.2}/tests/test_lifecycle_buffer.py +38 -1
  37. {topicforge-0.5.0 → topicforge-0.5.2}/tests/test_metrics_buffer.py +85 -0
  38. {topicforge-0.5.0 → topicforge-0.5.2}/tests/test_opendds_adapter.py +4 -2
  39. {topicforge-0.5.0 → topicforge-0.5.2}/tests/test_qos_analyzer.py +21 -0
  40. topicforge-0.5.2/tests/test_qos_endpoints.py +156 -0
  41. {topicforge-0.5.0 → topicforge-0.5.2}/tests/test_tools_integration.py +23 -0
  42. topicforge-0.5.0/src/topicforge/services/constants.py +0 -21
  43. {topicforge-0.5.0 → topicforge-0.5.2}/.gitignore +0 -0
  44. {topicforge-0.5.0 → topicforge-0.5.2}/LICENSE +0 -0
  45. {topicforge-0.5.0 → topicforge-0.5.2}/docs/MIGRATION_v0.1_to_v0.2.md +0 -0
  46. {topicforge-0.5.0 → topicforge-0.5.2}/docs/MIGRATION_v0.2_to_v0.3.md +0 -0
  47. {topicforge-0.5.0 → topicforge-0.5.2}/docs/MIGRATION_v0.3_to_v0.4.md +0 -0
  48. {topicforge-0.5.0 → topicforge-0.5.2}/docs/TESTING.md +0 -0
  49. {topicforge-0.5.0 → topicforge-0.5.2}/docs/TROUBLESHOOTING.md +0 -0
  50. {topicforge-0.5.0 → topicforge-0.5.2}/docs/dds-interop-matrix.md +0 -0
  51. {topicforge-0.5.0 → topicforge-0.5.2}/docs/pro.md +0 -0
  52. {topicforge-0.5.0 → topicforge-0.5.2}/examples/README.md +0 -0
  53. {topicforge-0.5.0 → topicforge-0.5.2}/scripts/integration/README.md +0 -0
  54. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/__main__.py +0 -0
  55. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/adapters/__init__.py +0 -0
  56. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/adapters/base.py +0 -0
  57. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/adapters/composite.py +0 -0
  58. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/adapters/dds_cyclone/__init__.py +0 -0
  59. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/adapters/dds_dust/__init__.py +0 -0
  60. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/adapters/dds_fast/__init__.py +0 -0
  61. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/adapters/dds_opendds/__init__.py +0 -0
  62. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/adapters/ros2_live/__init__.py +0 -0
  63. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/adapters/ros2_live/adapter.py +0 -0
  64. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/adapters/ros2_mock/__init__.py +0 -0
  65. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/adapters/ros2_mock/fixtures.py +0 -0
  66. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/config/__init__.py +0 -0
  67. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/config/settings.py +0 -0
  68. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/models/__init__.py +0 -0
  69. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/models/schemas.py +0 -0
  70. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/server/__init__.py +0 -0
  71. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/server/app.py +0 -0
  72. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/services/__init__.py +0 -0
  73. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/services/factory.py +0 -0
  74. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/telemetry/__init__.py +0 -0
  75. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/telemetry/client.py +0 -0
  76. {topicforge-0.5.0 → topicforge-0.5.2}/src/topicforge/tools/__init__.py +0 -0
  77. {topicforge-0.5.0 → topicforge-0.5.2}/tests/__init__.py +0 -0
  78. {topicforge-0.5.0 → topicforge-0.5.2}/tests/conftest.py +0 -0
  79. {topicforge-0.5.0 → topicforge-0.5.2}/tests/fixtures/csv_echo_imu.txt +0 -0
  80. {topicforge-0.5.0 → topicforge-0.5.2}/tests/fixtures/csv_echo_pose_multi.txt +0 -0
  81. {topicforge-0.5.0 → topicforge-0.5.2}/tests/integration/__init__.py +0 -0
  82. {topicforge-0.5.0 → topicforge-0.5.2}/tests/integration/conftest.py +0 -0
  83. {topicforge-0.5.0 → topicforge-0.5.2}/tests/integration/scenarios/lifecycle_tracking.json +0 -0
  84. {topicforge-0.5.0 → topicforge-0.5.2}/tests/integration/scenarios/multi_vendor_basic.json +0 -0
  85. {topicforge-0.5.0 → topicforge-0.5.2}/tests/integration/scenarios/qos_mismatch_detection.json +0 -0
  86. {topicforge-0.5.0 → topicforge-0.5.2}/tests/integration/scenarios/topic_metrics_frequency.json +0 -0
  87. {topicforge-0.5.0 → topicforge-0.5.2}/tests/integration/scenarios/topic_metrics_sequence_gaps.json +0 -0
  88. {topicforge-0.5.0 → topicforge-0.5.2}/tests/integration/scenarios/xtypes_decode.json +0 -0
  89. {topicforge-0.5.0 → topicforge-0.5.2}/tests/integration/test_real_bus.py +0 -0
  90. {topicforge-0.5.0 → topicforge-0.5.2}/tests/test_analyze_bag_multi_format.py +0 -0
  91. {topicforge-0.5.0 → topicforge-0.5.2}/tests/test_composite_adapter.py +0 -0
  92. {topicforge-0.5.0 → topicforge-0.5.2}/tests/test_config.py +0 -0
  93. {topicforge-0.5.0 → topicforge-0.5.2}/tests/test_cyclone_adapter.py +0 -0
  94. {topicforge-0.5.0 → topicforge-0.5.2}/tests/test_dds_cross_vendor.py +0 -0
  95. {topicforge-0.5.0 → topicforge-0.5.2}/tests/test_dds_schemas.py +0 -0
  96. {topicforge-0.5.0 → topicforge-0.5.2}/tests/test_dust_adapter.py +0 -0
  97. {topicforge-0.5.0 → topicforge-0.5.2}/tests/test_factory.py +0 -0
  98. {topicforge-0.5.0 → topicforge-0.5.2}/tests/test_fast_adapter.py +0 -0
  99. {topicforge-0.5.0 → topicforge-0.5.2}/tests/test_inspector.py +0 -0
  100. {topicforge-0.5.0 → topicforge-0.5.2}/tests/test_live_adapter_parse.py +0 -0
  101. {topicforge-0.5.0 → topicforge-0.5.2}/tests/test_live_adapter_subprocess.py +0 -0
  102. {topicforge-0.5.0 → topicforge-0.5.2}/tests/test_mock_adapter.py +0 -0
  103. {topicforge-0.5.0 → topicforge-0.5.2}/tests/test_peek_bag_samples.py +0 -0
  104. {topicforge-0.5.0 → topicforge-0.5.2}/tests/test_pro_hook.py +0 -0
  105. {topicforge-0.5.0 → topicforge-0.5.2}/tests/test_telemetry.py +0 -0
  106. {topicforge-0.5.0 → topicforge-0.5.2}/tests/test_topic_metrics.py +0 -0
  107. {topicforge-0.5.0 → topicforge-0.5.2}/tests/test_xtypes.py +0 -0
@@ -7,6 +7,198 @@ and this project adheres to [Semantic Versioning 2.0.0](https://semver.org/spec/
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.2] - 2026-08-22
11
+
12
+ Documentation and metadata only. No code, no schema, and no dependency
13
+ changes — `pip install topicforge` behaves identically to 0.5.1.
14
+
15
+ ### Added
16
+
17
+ - **`server.json` — MCP Registry metadata.** Declares the server as
18
+ `io.github.yaniswav/topicforge` (the `io.github.<user>/` prefix is
19
+ required by the registry's GitHub-namespace authentication) and points
20
+ at the PyPI package, with the four user-facing environment variables
21
+ described so MCP clients can render configuration hints. Validated
22
+ against the registry's published JSON schema.
23
+ - **PyPI ownership marker in `README.md`.** The registry verifies
24
+ ownership of a PyPI package by looking for an `mcp-name: <server name>`
25
+ string in the package description, which is generated from the README
26
+ at build time. The marker is an HTML comment, so it does not render.
27
+
28
+ ### Why this release exists
29
+
30
+ The marker only reaches PyPI when a distribution is built and published.
31
+ 0.5.1 shipped before the marker was added, so its published description
32
+ does not carry it and registry validation would reject the submission.
33
+ Cutting this version is the mechanical prerequisite for listing
34
+ TopicForge in the official MCP Registry — there is no functional change
35
+ to install.
36
+
37
+ ## [0.5.1] - 2026-08-22
38
+
39
+ ### Fixed (hotfix — MCP SDK 2.0 incompatibility)
40
+
41
+ - **Hard-pinned `mcp < 2` — TopicForge was uninstallable from 2026-07-28
42
+ to 2026-08-22.** The MCP Python SDK released `2.0.0` on 2026-07-28
43
+ alongside the `2026-07-28` protocol revision. That release removes the
44
+ `mcp.server.fastmcp` module entirely (`mcp/server/` now ships `auth`,
45
+ `lowlevel`, and `mcpserver`), so `server/app.py`'s
46
+ `from mcp.server.fastmcp import FastMCP` raises `ImportError` against
47
+ it. The dependency was declared as an unbounded `mcp>=1.0.0`, so every
48
+ fresh `pip install topicforge` resolved to `2.0.0` and produced a
49
+ server that could not start. Local development and CI both masked the
50
+ break — the dev environment had `mcp 1.27.1` already installed, and
51
+ the last CI run predates the SDK release. Pinning `<2` restores
52
+ installability on the 1.x line; migrating to the 2.x API
53
+ (`FastMCP` -> `MCPServer`, transport options moved from the
54
+ constructor to `.run()`, stateless protocol) is tracked separately and
55
+ is deliberately **not** bundled into this hotfix.
56
+
57
+ ### Note on scope
58
+
59
+ - This release also publishes the external-audit work (Lots 0-5,
60
+ 2026-07-08) that had accumulated under `[Unreleased]`. The Cyclone
61
+ `take_iter` -> `read_iter` change documented below is still **validated
62
+ by static analysis only** (`ruff` + `py_compile`); it has not been
63
+ exercised against a real multi-vendor DDS bus. It ships here because
64
+ leaving the package uninstallable was the larger harm — a broken
65
+ install affects every user, while this change can only affect users
66
+ running the Cyclone backend against a live bus. Real-bus validation
67
+ remains open.
68
+
69
+ ### Changed
70
+
71
+ - **DDS pure logic extracted for testability (Lot 0, external audit
72
+ 2026-07-08).** The QoS-profile normalizers (`_cyclone_qos_to_profile` /
73
+ `_fast_qos_to_profile`) and the discovery-sample field extractors
74
+ (`_extract_guid` / `_extract_vendor_id` / `_extract_hostname` /
75
+ `_extract_topic_name` / `_is_removal`) were moved out of
76
+ `adapters/dds_cyclone/adapter.py` and `adapters/dds_fast/adapter.py` —
77
+ which import their vendor binding at module top level and were therefore
78
+ never exercised by the test suite — into the binding-free
79
+ `adapters/common/qos_normalize.py` and `adapters/common/dds_introspection.py`.
80
+ The adapters import them back under their original private names, so every
81
+ call site is unchanged (verified by `ruff check` static analysis, since the
82
+ adapters are not importable without their SDKs). `fast_qos_to_profile`
83
+ takes the binding's int→str enum maps as parameters so it stays
84
+ import-free. This is the highest-value item from the audit: the QoS
85
+ normalization feeds `detect_qos_mismatches` (the flagship DDS diagnostic)
86
+ and was previously untestable and untested.
87
+
88
+ ### Added
89
+
90
+ - `tests/test_dds_qos_normalization.py` and `tests/test_dds_introspection.py`
91
+ drive the extracted logic with synthetic duck-typed objects (no
92
+ `cyclonedds` / `fastdds` needed), including regression guards for the
93
+ "renamed policy key silently yields no QoS profile → no mismatch ever
94
+ reported" failure mode. The extracted modules are now ~91–92% covered.
95
+ - `pytest-cov` and `rosbags` added to the `[dev]` extra, plus `[tool.coverage]`
96
+ config with a `fail_under = 85` floor (the two binding-only adapter shells
97
+ are `omit`ted as structurally unreachable without their SDKs). Adding
98
+ `rosbags` un-skips the real `.db3` bag-analysis I/O test.
99
+
100
+ ### Fixed
101
+
102
+ - `tests/test_bag_service.py` bag-generation helper updated for the current
103
+ `rosbags` API (`Writer(..., version=Writer.VERSION_LATEST)`; typestore keyed
104
+ by `std_msgs/msg/String`, not `std_msgs__msg__String`). The test was
105
+ previously auto-skipped and had gone silently stale — un-skipping it in CI
106
+ surfaced the drift.
107
+ - **`topic_metrics` frequency was wrong (Lot 2, audit C5).** It divided the
108
+ sample count by `(now − oldest_sample)` — folding in idle time since the
109
+ last peek — and counted N intervals instead of N−1. Now measured as
110
+ `(N−1) / (newest − oldest)` over the samples' own arrival span; samples
111
+ surfaced by a single opportunistic peek share one timestamp (span 0) and
112
+ correctly yield `frequency_hz_observed = null` instead of a fabricated rate.
113
+ - **`topic_metrics` sequence-gap count exploded on multi-writer topics,
114
+ publisher restarts, and counter wrap (Lot 2, audit C6).** Gaps are now
115
+ counted per writer (new best-effort `MetricsSample.writer_guid`), so
116
+ independent writers' counter offsets are not read as phantom gaps, and a
117
+ single jump wider than 10 000 is treated as a reset/wrap discontinuity
118
+ rather than that many losses.
119
+ - **QoS Deadline false negative (Lot 2, audit C3/P1-3).**
120
+ `detect_qos_mismatches` now flags a reader that requests a finite Deadline
121
+ against a writer that offers none — an absent deadline is the infinite
122
+ (loosest) period and cannot satisfy a finite request. The previous rule
123
+ required both sides non-null and silently missed this incompatibility.
124
+ - **`LifecycleBuffer` participant map was unbounded (Lot 3, audit P1-4 / M1 /
125
+ P1).** Only the event ring was capped; the participant dict grew one entry
126
+ per GUID ever seen (a churny bus mints a fresh RTPS GUID on each node
127
+ restart), and `list_participants` returned every tombstone forever. Now
128
+ capped at `MAX_PARTICIPANTS = 4096`, evicting `"left"` tombstones first then
129
+ the oldest-inserted entry — the docstring's "Bounded" claim is now true.
130
+ - **`MetricsBuffer` topic map was unbounded (Lot 3, audit P2-5).** Per-topic
131
+ rings were capped but the number of topic keys was not; now capped at
132
+ `MAX_TOPICS = 4096`, oldest-inserted topic evicted on overflow.
133
+ - **OpenDDS stub `is_available()` always returns False (Lot 4, audit S1).** A
134
+ stub that advertised availability (when a `pyopendds` module happened to be
135
+ importable) could be auto-selected by the factory, after which every tool
136
+ call raised. Now consistent with the Dust stub.
137
+ - **`iter_field_names` mis-decoded a string `__slots__` (Lot 4, audit C2).**
138
+ `__slots__ = "value"` was exploded into `['v','a','l','u','e']`; a bare
139
+ string slot is now treated as a single field name.
140
+ - **`decode_field_value` recursion is depth-capped at 32 (Lot 4, audit M6).**
141
+ A pathologically deep decoded object graph collapses to `repr()` instead of
142
+ risking `RecursionError`.
143
+ - **`_encode_raw_bytes` slices before hex-encoding (Lot 4, audit M5).** A large
144
+ raw payload no longer allocates its full 2×-size hex string only to truncate
145
+ it to the 4096-char preview.
146
+
147
+ ### Added (Lot 4 — test hardening)
148
+
149
+ - End-to-end test that a failing tool call surfaces as an MCP error
150
+ (`ToolError`) rather than being masked as a success — pins the thin-handler
151
+ contract (CLAUDE.md §8, audit test-gap #4).
152
+ - Test pinning that every canonical vendor tag is a valid
153
+ `ParticipantInfo`/`ParticipantEvent.vendor` Literal (guards against
154
+ vendor-map ↔ schema drift, audit P2-3). Scenario allowlist `_KNOWN_TOOLS`
155
+ now includes the 11th tool `peek_bag_samples`.
156
+
157
+ ### Removed (Lot 4)
158
+
159
+ - Dead `annotate_full` / `annotate_partial` imports and the `_ = (...)`
160
+ unused-suppressor from `services/bag_service.py`.
161
+
162
+ ### Documentation (Lot 1 — reconcile the strategic source of truth)
163
+
164
+ - **`docs/product-plan.md` realigned on the shipped 11-tool surface (audit
165
+ M6/P1-6).** §1 and §4 said "five typed tools today" / DDS "roadmapped"
166
+ while six DDS/observability tools had shipped across v0.2.0–v0.4.0. §11's
167
+ risk register carried a self-imposed governance gate — "any 9th tool needs
168
+ an explicit re-scope discussion documented in this register before code
169
+ lands" — that was crossed during v0.4.0 without the discussion being
170
+ recorded. Added a retroactive re-scope decision closing that gap: the three
171
+ ceiling-breaking tools are accepted, the new ceiling is 11 tools, a 12th
172
+ needs a documented re-scope.
173
+ - **User-topic `raw` decode honesty (audit C1).** README and the
174
+ `peek_dds_samples` tool description no longer imply the `raw` fallback
175
+ preserves the payload in `_raw_bytes_hex` — on the current user-topic raw
176
+ path that field is empty (a `raw` status means "present but not decoded").
177
+ Capturing the on-wire CDR bytes is stated as roadmapped rather than done.
178
+
179
+ ### Changed (Lot 5 — DDS adapter deduplication)
180
+
181
+ - **QoS-mismatch endpoint pairing deduplicated (audit D1/M7/P2).** The ~40
182
+ identical lines in each adapter's `detect_qos_mismatches` (group endpoints by
183
+ topic, pair reader × writer, build `MismatchReport`) moved to the binding-free
184
+ `common/qos_endpoints.detect_mismatches_across_endpoints`, unit-tested without
185
+ a binding. Each writer's QoS profile is now parsed once per topic instead of
186
+ once per reader (fixes the O(readers × writers) re-parse). Both adapters
187
+ delegate to it.
188
+ - **Shared `validate_domain_id` (`common/dds_helpers`).** The identical 0..232
189
+ bound check in all four DDS adapter constructors (Cyclone, Fast, OpenDDS,
190
+ Dust) is now defined once.
191
+ - **Cyclone discovery/sample reads switched from `take_iter` to `read_iter`
192
+ (audit A1/P1-5).** Destructive `take` drained the builtin discovery cache,
193
+ risking spurious lost / re-discovered participant flapping across polls;
194
+ `read` is non-destructive — the correct choice for read-only observability.
195
+ ⚠️ **Requires real-bus validation on `scripts/integration/` before release**:
196
+ the read-vs-take semantics cannot be exercised without `cyclonedds` installed
197
+ (the adapter is not importable in the unit environment; these edits are
198
+ validated only by `ruff` static analysis + `py_compile`).
199
+
200
+ Baseline: 399 → 485 passed, 24 → 23 skipped, ruff clean, coverage 89.12%.
201
+
10
202
  ## [0.5.0] - 2026-05-21
11
203
 
12
204
  ### Sprint v0.5.0 — Polish + validation (pre-marketing-publication)
@@ -713,7 +905,9 @@ Initial MVP release of TopicForge — ROS Topic Inspector & Bag Analyzer MCP ser
713
905
  - The write path (publishing, commanding robots) is intentionally out of scope for the MVP.
714
906
  - `analyze_bag` in live mode parses `ros2 bag info` text output; deeper anomaly detection remains mock-only for now.
715
907
 
716
- [Unreleased]: https://github.com/yaniswav/TopicForge/compare/v0.5.0...HEAD
908
+ [Unreleased]: https://github.com/yaniswav/TopicForge/compare/v0.5.2...HEAD
909
+ [0.5.2]: https://github.com/yaniswav/TopicForge/compare/v0.5.1...v0.5.2
910
+ [0.5.1]: https://github.com/yaniswav/TopicForge/compare/v0.5.0...v0.5.1
717
911
  [0.5.0]: https://github.com/yaniswav/TopicForge/compare/v0.4.0...v0.5.0
718
912
  [0.4.0]: https://github.com/yaniswav/TopicForge/compare/v0.3.0...v0.4.0
719
913
  [0.3.0]: https://github.com/yaniswav/TopicForge/compare/v0.2.0...v0.3.0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: topicforge
3
- Version: 0.5.0
3
+ Version: 0.5.2
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
@@ -39,7 +39,7 @@ Classifier: Programming Language :: Python :: 3.11
39
39
  Classifier: Programming Language :: Python :: 3.12
40
40
  Classifier: Programming Language :: Python :: 3.13
41
41
  Requires-Python: >=3.11
42
- Requires-Dist: mcp>=1.0.0
42
+ Requires-Dist: mcp<2,>=1.0.0
43
43
  Requires-Dist: pydantic>=2.6
44
44
  Provides-Extra: all
45
45
  Requires-Dist: cyclonedds>=0.10; extra == 'all'
@@ -63,12 +63,16 @@ Requires-Dist: fastdds<3,>=2.6.1; extra == 'dds-fast'
63
63
  Provides-Extra: dds-opendds
64
64
  Requires-Dist: pyopendds>=0.1; extra == 'dds-opendds'
65
65
  Provides-Extra: dev
66
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
66
67
  Requires-Dist: pytest>=8.0; extra == 'dev'
68
+ Requires-Dist: rosbags>=0.9; extra == 'dev'
67
69
  Requires-Dist: ruff>=0.4; extra == 'dev'
68
70
  Description-Content-Type: text/markdown
69
71
 
70
72
  # TopicForge
71
73
 
74
+ <!-- mcp-name: io.github.yaniswav/topicforge -->
75
+
72
76
  [![PyPI version](https://img.shields.io/pypi/v/topicforge.svg)](https://pypi.org/project/topicforge/)
73
77
  [![CI](https://github.com/yaniswav/TopicForge/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/yaniswav/TopicForge/actions/workflows/ci.yml)
74
78
  [![Python versions](https://img.shields.io/pypi/pyversions/topicforge.svg)](https://pypi.org/project/topicforge/)
@@ -254,7 +258,7 @@ Six DDS / observability tools (in addition to the five ROS2 tools above) :
254
258
 
255
259
  **Composite adapter (v0.4.0 Phase 1+).** When `TOPICFORGE_MODE=live` is paired with a DDS backend (`cyclone`, `fast`, …), TopicForge instantiates **both** a ROS2 CLI adapter and the chosen DDS adapter and routes per-tool category — the 5 ROS2 tools hit the CLI, the DDS / observability tools hit the DDS backend. ROS2-only or DDS-only setups still work — the missing half is skipped and the present half serves what it can. The mock backend continues to expose all 11 tools against deterministic fixtures for local development.
256
260
 
257
- **`peek_dds_samples` payload shape (v0.4.0 Phase 1.5).** Full-fidelity on the 4 builtin DCPS topics (`DCPSParticipant`, `DCPSSubscription`, `DCPSPublication`). Arbitrary user topics return best-effort decoded samples with a `_decode_status` annotation : `"full"` (every IDL field decoded — currently a v0.4.0+ Cyclone XTypes path), `"partial"` (some fields decoded, others opaque), or `"raw"` (binding could not resolve the dynamic XTypes — bytes preserved as hex in `_raw_bytes_hex`). The diagnostic key `_decode_note` carries a short explanation when the status is non-`full`. The wire shape is identical across Cyclone and Fast backends.
261
+ **`peek_dds_samples` payload shape (v0.4.0 Phase 1.5).** Full-fidelity on the 4 builtin DCPS topics (`DCPSParticipant`, `DCPSSubscription`, `DCPSPublication`). Arbitrary user topics return best-effort decoded samples with a `_decode_status` annotation : `"full"` (every IDL field decoded — currently a v0.4.0+ Cyclone XTypes path), `"partial"` (some fields decoded, others opaque), or `"raw"` (binding could not resolve the dynamic XTypes). The diagnostic key `_decode_note` carries a short explanation when the status is non-`full`. The wire shape is identical across Cyclone and Fast backends. **Caveat (v0.5.x):** on the current user-topic *raw* path `_raw_bytes_hex` is **empty** — a `"raw"` status means "topic present on the bus but not decoded", not "here are the serialized bytes to re-decode". Capturing the on-wire CDR bytes into the fallback is roadmapped ; until then use the Cyclone full/partial XTypes path for actual user-topic payloads. The 4 builtin DCPS topics are unaffected (always structured).
258
262
 
259
263
  **`RTI Connext`** is v0.4.0+ Pro tier (BYO license — see `docs/pro.md`).
260
264
 
@@ -1,5 +1,7 @@
1
1
  # TopicForge
2
2
 
3
+ <!-- mcp-name: io.github.yaniswav/topicforge -->
4
+
3
5
  [![PyPI version](https://img.shields.io/pypi/v/topicforge.svg)](https://pypi.org/project/topicforge/)
4
6
  [![CI](https://github.com/yaniswav/TopicForge/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/yaniswav/TopicForge/actions/workflows/ci.yml)
5
7
  [![Python versions](https://img.shields.io/pypi/pyversions/topicforge.svg)](https://pypi.org/project/topicforge/)
@@ -185,7 +187,7 @@ Six DDS / observability tools (in addition to the five ROS2 tools above) :
185
187
 
186
188
  **Composite adapter (v0.4.0 Phase 1+).** When `TOPICFORGE_MODE=live` is paired with a DDS backend (`cyclone`, `fast`, …), TopicForge instantiates **both** a ROS2 CLI adapter and the chosen DDS adapter and routes per-tool category — the 5 ROS2 tools hit the CLI, the DDS / observability tools hit the DDS backend. ROS2-only or DDS-only setups still work — the missing half is skipped and the present half serves what it can. The mock backend continues to expose all 11 tools against deterministic fixtures for local development.
187
189
 
188
- **`peek_dds_samples` payload shape (v0.4.0 Phase 1.5).** Full-fidelity on the 4 builtin DCPS topics (`DCPSParticipant`, `DCPSSubscription`, `DCPSPublication`). Arbitrary user topics return best-effort decoded samples with a `_decode_status` annotation : `"full"` (every IDL field decoded — currently a v0.4.0+ Cyclone XTypes path), `"partial"` (some fields decoded, others opaque), or `"raw"` (binding could not resolve the dynamic XTypes — bytes preserved as hex in `_raw_bytes_hex`). The diagnostic key `_decode_note` carries a short explanation when the status is non-`full`. The wire shape is identical across Cyclone and Fast backends.
190
+ **`peek_dds_samples` payload shape (v0.4.0 Phase 1.5).** Full-fidelity on the 4 builtin DCPS topics (`DCPSParticipant`, `DCPSSubscription`, `DCPSPublication`). Arbitrary user topics return best-effort decoded samples with a `_decode_status` annotation : `"full"` (every IDL field decoded — currently a v0.4.0+ Cyclone XTypes path), `"partial"` (some fields decoded, others opaque), or `"raw"` (binding could not resolve the dynamic XTypes). The diagnostic key `_decode_note` carries a short explanation when the status is non-`full`. The wire shape is identical across Cyclone and Fast backends. **Caveat (v0.5.x):** on the current user-topic *raw* path `_raw_bytes_hex` is **empty** — a `"raw"` status means "topic present on the bus but not decoded", not "here are the serialized bytes to re-decode". Capturing the on-wire CDR bytes into the fallback is roadmapped ; until then use the Cyclone full/partial XTypes path for actual user-topic payloads. The 4 builtin DCPS topics are unaffected (always structured).
189
191
 
190
192
  **`RTI Connext`** is v0.4.0+ Pro tier (BYO license — see `docs/pro.md`).
191
193
 
@@ -95,7 +95,7 @@ Both real backends and the mock fixtures encode the canonical "subscriber doesn'
95
95
 
96
96
  An LLM reading this output has enough information to suggest a concrete fix ("the writer is BEST_EFFORT but the reader requires RELIABLE — either relax the reader or upgrade the writer"). That is the diagnostic loop the DDS module is designed to support — and it works identically regardless of which backend produced the discovery samples, because the vendor-neutral pure analyzer at `src/topicforge/adapters/common/qos_analyzer.py` operates on canonical `QosProfile` Pydantic models.
97
97
 
98
- The analyzer covers the four MVP policies — **Reliability**, **Durability**, **History**, **Deadline** — that explain the bulk of real-world mismatch cases. Liveliness, Ownership, Partition, TimeBasedFilter, and LatencyBudget are v0.3.x patches.
98
+ The analyzer covers the four MVP policies — **Reliability**, **Durability**, **History**, **Deadline** — that explain the bulk of real-world mismatch cases. Liveliness, Ownership, Partition, TimeBasedFilter, and LatencyBudget are v0.5.x patches.
99
99
 
100
100
  ---
101
101
 
@@ -0,0 +1,204 @@
1
+ # TopicForge — Tutorial
2
+
3
+ TopicForge is a read-only Model Context Protocol (MCP) server that gives an AI agent grounded, structured visibility into a ROS2 robotics stack and the raw DDS layer beneath it — topics, participants, QoS, recorded bags — without ever being able to publish, call a service, or command a robot.
4
+
5
+ **Who this is for.** ROS2 developers, robotics ML/CV engineers, and anyone who wants their AI assistant to answer "what's actually happening on my robot's graph right now" instead of guessing from training data.
6
+
7
+ **The read-only guarantee, in one sentence.** There is no write path anywhere in TopicForge's architecture — not a locked-down permission you could misconfigure, but code that was never written, so there is nothing to flip and nothing to exploit into a write.
8
+
9
+ This tutorial covers installing TopicForge, using its eleven tools, and wiring it into a recurring monitoring workflow. For OS-by-OS environment setup (WSL2, native Linux, Docker, native Windows), see [`TESTING.md`](TESTING.md).
10
+
11
+ ---
12
+
13
+ ## Quickstart
14
+
15
+ Requires Python 3.11+. This section needs no ROS2 install — the mock adapter serves deterministic fixtures for a small demo robot, so you can try every tool cold.
16
+
17
+ ```bash
18
+ pip install topicforge
19
+ ```
20
+
21
+ Add it to your MCP client. For Claude Desktop, edit `claude_desktop_config.json`:
22
+
23
+ ```json
24
+ {
25
+ "mcpServers": {
26
+ "topicforge": {
27
+ "command": "topicforge",
28
+ "env": { "TOPICFORGE_MODE": "mock" }
29
+ }
30
+ }
31
+ }
32
+ ```
33
+
34
+ Restart Claude Desktop. All eleven tools appear under the hammer icon. Ask something like:
35
+
36
+ > What topics are being published right now, and what message types do they carry?
37
+
38
+ The agent calls `list_topics`. In mock mode you'll see the five fixture topics of the demo robot — `/cmd_vel`, `/odom`, `/scan`, `/tf`, `/camera/image_raw` — deterministic across runs, so you can build a mental model of the tool surface before pointing it at a real graph.
39
+
40
+ When you're ready for a real ROS2 environment, switch `TOPICFORGE_MODE` to `live` or `auto` — see [Advanced options](#advanced-options) below, and [`TESTING.md`](TESTING.md) for full setup paths per OS.
41
+
42
+ **Windows note.** File paths in this document are shown in POSIX style (`/tmp/demo.mcap`) because that's the form MCP clients pass internally, but TopicForge itself runs natively on Windows. Path resolution goes through `pathlib`, so both `C:\demos\run.mcap` and `C:/demos/run.mcap` work when you pass a bag path yourself.
43
+
44
+ ---
45
+
46
+ ## The eleven tools
47
+
48
+ | Tool | What it does | When to use it |
49
+ | --- | --- | --- |
50
+ | `health_check` | Reports effective mode (`live`/`mock`), whether `ros2` is on PATH, the active DDS backend, and other environment state. Always succeeds. | Call first when something looks wrong — every other tool can raise an error, this one won't. |
51
+ | `list_topics` | Lists every topic on the current ROS2 graph. | Discover what's currently being published before drilling into anything specific. |
52
+ | `get_topic_info` | Structured detail for one topic — message type, publisher/subscriber counts, QoS reliability. | Check a topic's shape and who's connected to it before subscribing or debugging. |
53
+ | `sample_messages` | Peeks recent messages on a ROS2 topic. | See real payload content without shelling out to `ros2 topic echo` yourself. |
54
+ | `analyze_bag` | Summarizes a `.mcap` / `.db3` / `.bag` recording — duration, message count, per-topic stats. | Get a quick overview of a recorded run before deciding whether to dig deeper. |
55
+ | `list_participants` | Lists DDS participants observed on a domain, at the raw DDS layer beneath ROS. | Diagnose why a participant isn't visible to the ROS graph, or inspect a non-ROS DDS stack. |
56
+ | `detect_qos_mismatches` | Finds incompatible QoS pairs between DDS readers and writers. | A subscriber isn't receiving despite an apparently healthy publisher — this tells you why. |
57
+ | `peek_dds_samples` | Peeks raw DDS samples, including the DCPS builtin discovery topics and (best-effort decoded) user topics. | Inspect non-ROS DDS topics, or the discovery layer itself. |
58
+ | `participant_events` | Timeline of participant discovered/lost events over a lookback window. | Investigate churn — nodes restarting, dropping off, or new ones joining. |
59
+ | `topic_metrics` | Observed frequency, latency percentiles, and sequence gaps for a topic over a time window. | Check whether a topic is actually meeting its declared publish rate or QoS Deadline. |
60
+ | `peek_bag_samples` | Peeks recent samples from a recorded bag file (MCAP, `.db3`, or legacy ROS1 `.bag`). | Inspect payload content from a past recording without replaying the whole bag. |
61
+
62
+ ---
63
+
64
+ ## Advanced options
65
+
66
+ ### Environment variables
67
+
68
+ | Variable | Default | Purpose |
69
+ | --- | --- | --- |
70
+ | `TOPICFORGE_MODE` | `auto` | Selects `mock`, `live`, or `auto` (see below). |
71
+ | `TOPICFORGE_LOG_LEVEL` | `INFO` | `DEBUG`, `INFO`, `WARNING`, or `ERROR`. |
72
+ | `TOPICFORGE_ROS2_BIN` | `ros2` | Overrides the resolved `ros2` executable — useful when it isn't on PATH. |
73
+ | `TOPICFORGE_TELEMETRY` | off | Opt-in anonymous telemetry. On-values: `on`, `1`, `true`, `yes`, `enabled`. See [Privacy](#privacy). |
74
+ | `TOPICFORGE_DDS_BACKEND` | `mock` | Selects the DDS backend: `mock`, `cyclone`, `fast`, or `auto`. A few more identifiers exist in the settings schema (`rti`, `opensplice`, `coredx`, `intercom`, `opendds`, `dust`) — see [Choosing a DDS backend](#choosing-a-dds-backend). |
75
+ | `TOPICFORGE_DDS_DOMAIN_ID` | `0` | DDS domain id to observe, `0`–`232`. |
76
+
77
+ Add any of these to the same `env` object shown in the Quickstart config block.
78
+
79
+ ### The three modes
80
+
81
+ - **`mock`** — deterministic fixtures, no ROS2 or DDS SDK required. Use for development, demos, CI, or evaluating the tool surface before touching a real graph.
82
+ - **`live`** — talks to a real environment: the `ros2` CLI for the 5 ROS2 graph tools, and the configured DDS backend for the 6 DDS/observability tools. Use once ROS2 and/or a DDS backend are actually reachable from the shell that spawns TopicForge.
83
+ - **`auto`** (default) — resolves to `live` if the `ros2` executable is found on PATH, otherwise falls back to `mock`. The DDS backend has its own `auto` resolution (see below), independent of `TOPICFORGE_MODE`.
84
+
85
+ ### DDS extras, per vendor
86
+
87
+ Plain `pip install topicforge` gives you the 5 ROS2 tools plus mock fixtures for all eleven. To talk to a real DDS bus, install one of:
88
+
89
+ ```bash
90
+ pip install topicforge[dds-cyclone] # Eclipse CycloneDDS only
91
+ pip install topicforge[dds-fast] # eProsima Fast DDS only
92
+ pip install topicforge[dds] # both — the recommended default
93
+ pip install topicforge[all] # currently equivalent to [dds]
94
+ pip install topicforge[bags] # rosbags library — required for peek_bag_samples (no fallback);
95
+ # analyze_bag falls back to `ros2 bag info` text parsing without it
96
+ ```
97
+
98
+ `topicforge[dds-opendds]` and `topicforge[dds-dust]` also exist in `pyproject.toml`, but as of this writing they pin `pyopendds` and `dust-dds-python` — packages not currently maintained on PyPI. Installing either extra fails at `pip install` time; they exist so the auto-detect framework has a known module name to probe once upstream ships a working release. Don't rely on them yet.
99
+
100
+ ### Choosing a DDS backend
101
+
102
+ Set `TOPICFORGE_DDS_BACKEND` explicitly (`cyclone` or `fast`) if you have a preference and both are installed, or leave it on `auto` and let TopicForge pick. In practice, for the free tier `auto` resolves to Fast DDS if installed, otherwise CycloneDDS if installed, otherwise `mock`. The full priority chain also probes Pro-tier vendors (`rti`, `opensplice`, `coredx`, `intercom`) first, but only if you've separately installed the `topicforge-pro` add-on — without it, those probes are skipped automatically and fall through to the OSS vendors. Selecting `rti` explicitly without the Pro add-on falls back to the ROS2-CLI-only adapter with a logged warning rather than failing outright. `opendds` and `dust` are recognized identifiers with no working install path today (see the extras caveat above).
103
+
104
+ ### Domain id
105
+
106
+ `TOPICFORGE_DDS_DOMAIN_ID` (default `0`, matching the default used by most ROS2 setups and by `cyclonedds` itself) sets the domain a live DDS adapter joins **at startup** — it does not change per call. `list_participants`, `participant_events`, and `topic_metrics` also accept a `domain_id` parameter, but on the real Cyclone/Fast adapters this parameter is accepted for protocol uniformity only; the adapter still reports what it sees on the domain it joined at construction time, not a different domain picked per call. To observe a different domain, restart the server with a different `TOPICFORGE_DDS_DOMAIN_ID`.
107
+
108
+ ---
109
+
110
+ ## Scheduled-task prompts
111
+
112
+ TopicForge's tools are read-only, which makes them safe to run unattended on a schedule — a cron-triggered agent session, a scheduled task in whatever orchestrator you use, or a recurring reminder in your MCP client. The prompts below are generic: swap in your own topic names, domain ids, and bag paths. They're written as instructions to hand an agent, not as commands to run yourself.
113
+
114
+ **Catches:** silent reader/writer QoS incompatibilities that block delivery — introduced by a new node, a config change, or a vendor default drifting from the rest of the bus.
115
+
116
+ ```
117
+ Run detect_qos_mismatches across the whole bus (no topic filter). For every
118
+ result with severity "incompatible", explain in plain language which QoS
119
+ policy is blocking delivery, which side (reader or writer) is the outlier,
120
+ and the smallest concrete change that would fix it (e.g. "relax the reader
121
+ to BEST_EFFORT" or "raise the writer to TRANSIENT_LOCAL durability"). List
122
+ any "risky" (non-blocking) mismatches separately as a lower-priority note.
123
+ If nothing is found, say so in one line — do not pad the report.
124
+ ```
125
+
126
+ **Catches:** environment drift before you trust the stack in the field — wrong mode silently active, `ros2` not actually on PATH, DDS backend quietly falling back to mock.
127
+
128
+ ```
129
+ Before I start today's run, call health_check and confirm: the effective
130
+ mode is "live" (not silently "mock"), the ros2 CLI is detected, and the
131
+ active DDS backend matches what I expect. Then call list_topics and
132
+ list_participants(domain_id=0), and tell me if the topic count or
133
+ participant count looks abnormally low compared to a normal startup.
134
+ Flag anything that looks like a partial or degraded environment before
135
+ I rely on it.
136
+ ```
137
+
138
+ **Catches:** flapping nodes (crash-restart loops), unexpected disconnects mid-run, or a new participant joining the bus that nobody deployed on purpose.
139
+
140
+ ```
141
+ Call participant_events(domain_id=0, lookback_seconds=3600) and summarize
142
+ the last hour of DDS participant activity. Group by guid: which
143
+ participants were discovered then lost more than once (possible crash
144
+ loop), which are new, and which have been stable the whole window. Call
145
+ out anything under a hostname or vendor you don't recognize.
146
+ ```
147
+
148
+ **Catches:** a critical topic silently dropping below its declared publish rate, latency creeping up, or sequence gaps appearing — degradation that doesn't throw an error but breaks downstream consumers.
149
+
150
+ ```
151
+ For each of these topics: <topic-1>, <topic-2>, <topic-3> — first call
152
+ peek_dds_samples(topic=<topic>, count=10) to warm up the metrics buffer,
153
+ then call topic_metrics(topic=<topic>, window_seconds=60) and report
154
+ frequency_hz_observed against frequency_hz_declared, the p50/p95/p99
155
+ latency, and sequence_gaps_count. Flag any topic where the observed
156
+ frequency is more than 20% below the declared rate, or where
157
+ sequence_gaps_count is greater than zero.
158
+ ```
159
+
160
+ **Catches:** recorded test or field runs that only get inspected reactively after something breaks, instead of on a regular cadence — letting anomalies (duration mismatches, missing topics, gaps) surface while they're still cheap to investigate.
161
+
162
+ ```
163
+ Analyze the bag at <path-to-bag>. Report duration, total message count,
164
+ and per-topic message counts. Then peek 5 samples from <topic-of-interest>
165
+ in that bag and tell me if the payload shape looks consistent with a
166
+ normal run. Flag anything anomalous: missing topics you'd expect to see,
167
+ a duration much shorter or longer than usual, or a topic with a
168
+ suspiciously low message count for its expected rate.
169
+ ```
170
+
171
+ ---
172
+
173
+ ## Privacy
174
+
175
+ TopicForge is built so the question "what does this send off my machine?" has a short, verifiable answer.
176
+
177
+ ### Telemetry
178
+
179
+ Telemetry is **off by default**. It's opt-in via `TOPICFORGE_TELEMETRY=on` (accepted on-values: `on`, `1`, `true`, `yes`, `enabled` — anything else, including an unset variable, stays off).
180
+
181
+ When enabled, each tool call emits exactly six fields: `tool_name`, `latency_ms`, `mode`, `version`, `session_id`, `success`. `session_id` is a random id generated per server process — it isn't tied to your identity or any persistent identifier, and it's never written to disk.
182
+
183
+ When disabled — the default — this isn't "we choose not to send it," it's "the code path doesn't exist." The instrumentation wrapper returns the tool handler unmodified: no event object is built, no transport is constructed, no network code executes. This no-op behavior is covered by a dedicated test in the codebase.
184
+
185
+ ### What never leaves your machine
186
+
187
+ Regardless of the telemetry setting, TopicForge never transmits topic names, message payloads, bag file paths or contents, hostnames, or any other environment variable. The service and adapter layers that see this data have no code path into the telemetry module — by construction, not just by convention.
188
+
189
+ ### Bags are read locally
190
+
191
+ `analyze_bag` and `peek_bag_samples` open bag files on the filesystem where TopicForge runs and parse them in-process — via `ros2 bag info` in live mode, or the `rosbags` library when installed (`pip install topicforge[bags]`). Nothing about a bag's content or path is uploaded anywhere.
192
+
193
+ ### Read-only, restated
194
+
195
+ The same architectural property that keeps TopicForge from commanding your robot also bounds what it can leak: there is no write path anywhere in the codebase — not to the bus, not to a remote endpoint — other than the six-field telemetry event described above, and that event stays off unless you explicitly turn it on.
196
+
197
+ ---
198
+
199
+ ## Where to go next
200
+
201
+ - [`DDS_QUICKSTART.md`](DDS_QUICKSTART.md) — a deeper tour of the DDS module: mock vs. live walkthroughs per backend, the QoS mismatch scenario end-to-end, and the composite-adapter routing table.
202
+ - [`TESTING.md`](TESTING.md) — five paths to a working ROS2 environment (WSL2, native Linux, Docker, native Windows), plus MCP client wiring for Claude Desktop, Claude Code, and others.
203
+ - [`TROUBLESHOOTING.md`](TROUBLESHOOTING.md) — the codebase's polished error messages, what they mean, and how to fix them.
204
+ - [`dds-interop-matrix.md`](dds-interop-matrix.md) — why TopicForge sees participants from any OMG-DDS-RTPS-conformant vendor, not just the one it's bound to.
@@ -8,7 +8,7 @@
8
8
 
9
9
  TopicForge is **the safety-first read-only MCP for ROS2 robotics**. Where general-purpose ROS-MCP servers let an LLM publish topics, call services, and command robots — useful for demos, untenable for production fleets, defense systems, or anything safety-certified — TopicForge is read-only by **architecture**, not by configuration. There is no write path to misconfigure, no permission system to audit, no liability conversation to have. The MCP client can see the robot stack; it cannot touch it.
10
10
 
11
- Concretely, the server exposes five typed tools today — `health_check`, `list_topics`, `get_topic_info`, `sample_messages`, `analyze_bag` — backed by either a deterministic mock adapter (no ROS2 required) or a `ros2` CLI wrapper (full live introspection). Outputs are frozen Pydantic schemas, stable across runtime modes. Telemetry is opt-in, six fields, zero user payload.
11
+ Concretely, the server exposes **eleven typed read-only tools today** (v0.5.0): the five ROS2-graph tools (`health_check`, `list_topics`, `get_topic_info`, `sample_messages`, `analyze_bag`) plus the six DDS / observability tools shipped across v0.2.0–v0.4.0 (`list_participants`, `detect_qos_mismatches`, `peek_dds_samples`, `participant_events`, `topic_metrics`, `peek_bag_samples`). They are backed by a deterministic mock adapter (no ROS2/DDS required), a `ros2` CLI wrapper, or an OSS DDS participant (Eclipse CycloneDDS / eProsima Fast DDS). Outputs are frozen Pydantic schemas, stable across runtime modes. Telemetry is opt-in, six fields, zero user payload.
12
12
 
13
13
  The ROS-MCP category is no longer empty (see §11 Risk register for the competitive landscape as of 2026-05-13). What TopicForge defends, and the rest of the pack will inherit, is the read-only-by-architecture stance and the production-quality engineering envelope around it — frozen schemas, mock-first development, telemetry contract pinned by tests, Windows-first cross-platform, no shell injection, deterministic outputs.
14
14
 
@@ -40,13 +40,13 @@ Three concentric circles, ranked by strategic priority rather than acquisition c
40
40
 
41
41
  The strategic bet is **pack breadth via two focused products plus a modular surface inside TopicForge**. Two MCPs, not three to five — solo maintenance cost was the binding constraint and the 2026-05-14 audit collapsed the earlier 3-to-5-MCP plan accordingly.
42
42
 
43
- **MCP 01 — TopicForge umbrella.** Covers ROS2 introspection today (shipped v0.1.2) and DDS observability as the next module (roadmapped, see §8). The `RosAdapter` protocol generalizes into a `MiddlewareAdapter` protocol that supports CycloneDDS in the OSS core and RTI Connext under the existing `topicforge_pro` license-gated package. One install (`pip install topicforge`, optional extras for DDS), one CLI, one license key — the umbrella keeps the developer ergonomics tight while extending coverage to the DDS-native audience the ROS-MCP competitive set does not reach. Module spec at `docs/projet-file/mcp-02-spec.md`.
43
+ **MCP 01 — TopicForge umbrella.** Covers ROS2 introspection (shipped v0.1.2) and DDS observability, which **shipped as a module across v0.2.0–v0.4.0** (see §8) — no longer roadmapped. The `RosAdapter` protocol was generalized into a `MiddlewareAdapter` protocol that supports Eclipse CycloneDDS and eProsima Fast DDS in the OSS core and RTI Connext under the existing `topicforge_pro` license-gated package. One install (`pip install topicforge`, optional extras for DDS), one CLI, one license key — the umbrella keeps the developer ergonomics tight while extending coverage to the DDS-native audience the ROS-MCP competitive set does not reach. Module spec at `docs/projet-file/mcp-02-spec.md`.
44
44
 
45
45
  **MCP 02 — DatasetForge.** Vision Dataset Inspector. Read images + annotations (COCO at MVP; YOLO / HF Datasets on roadmap) and answer structured questions about class balance, split coherence, annotation quality. Targets the ML/CV audience overlapping with TopicForge but distinct enough in domain (training data vs runtime graph) to warrant a separate product, separate repo, separate PyPI name. Full spec at `docs/projet-file/mcp-03-spec.md` (the file is still named `mcp-03-spec.md` for historical continuity ; the slot is MCP 02 of the 2-MCP pack).
46
46
 
47
47
  **Motif of the pivot.** Earlier drafts of this plan sequenced a 3-to-5-MCP pack with a separate DDS observability MCP as MCP 02. The 2026-05-14 audit collapsed that into a 2-product strategy: TopicForge as an umbrella covering both middlewares, DatasetForge as the second standalone product. The binding constraints were (a) solo-maintenance cost of running two repos in parallel and (b) the fact that ROS2 and DDS are the same problem shape — a typed pub/sub graph that needs structured introspection — and the `RosAdapter` protocol already generalizes to a `MiddlewareAdapter` superset with zero rework. Two products instead of three reduces the surface area without losing coverage.
48
48
 
49
- The umbrella commits TopicForge to a slightly broader scope (cap: 5 ROS2 tools today + at most 3 DDS-side tools when the module ships, ceiling enforced by §11). The pack inherits the layer separation, mock-first development, opt-in telemetry, and read-only-by-architecture commitments from TopicForge. Pack-shared infrastructure extraction (telemetry, license, settings resolver into a `pack-template/` repo) becomes a non-decision at 2 products: fork-and-tweak from TopicForge to DatasetForge is acceptable ; revisit only if a third product is ever planned.
49
+ The umbrella commits TopicForge to a broader scope than first drafted: the DDS module shipped **six** DDS / observability tools across v0.2.0–v0.4.0 (not the three originally scoped), taking the surface to **11 tools total**. The original 8-tool ceiling was formally revised — see the re-scope decision in §11. The pack inherits the layer separation, mock-first development, opt-in telemetry, and read-only-by-architecture commitments from TopicForge. Pack-shared infrastructure extraction (telemetry, license, settings resolver into a `pack-template/` repo) becomes a non-decision at 2 products: fork-and-tweak from TopicForge to DatasetForge is acceptable ; revisit only if a third product is ever planned.
50
50
 
51
51
  ---
52
52
 
@@ -171,7 +171,7 @@ The risks worth tracking explicitly. Updated 2026-05-13 with the competitive lan
171
171
  - **Cross-platform regressions on Windows.** TopicForge's primary developer environment is Windows. The Makefile uses POSIX shell syntax; users on plain PowerShell need the documented escape hatches. Mitigation: tested directly in CI on `ubuntu-latest` only today; Windows coverage is documented in `docs/TESTING.md` and exercised manually before each release.
172
172
  - **Telemetry trust.** Even opt-in telemetry can damage trust if the payload contract drifts. Mitigation: `tests/test_telemetry.py::test_payload_contains_only_whitelisted_keys` pins the six allowed keys. Any change requires a CHANGELOG entry and a README Telemetry section update in the same PR.
173
173
  - **Time / focus dilution.** A solo maintainer trying to drive two products (TopicForge umbrella + DatasetForge), a Pro tier inside each, marketing, and the DDS module on top of TopicForge is the realistic risk. The 2026-05-14 pivot from a 3-to-5-MCP pack to a 2-product strategy reduced the surface but did not eliminate the risk. Mitigation: explicit phase gates (do not start Phase 2 until Phase 1 is shipped, do not act on the DDS module marketing until Phase 2 has shipped) — though §8 schedules `MiddlewareAdapter` protocol prep during Phase 1.
174
- - **Scope creep within the TopicForge umbrella.** Combining ROS2 + DDS introspection in one product risks bloating the tool surface beyond what a focused MCP should expose. Mitigation: tool surface stays capped at the 5 ROS2 tools today ; the DDS module adds at most 3 new tools (`list_participants`, `detect_qos_mismatches`, `peek_dds_samples`) when it ships. Any 9th tool needs an explicit re-scope discussion documented in this register before code lands.
174
+ - **Scope creep within the TopicForge umbrella.** Combining ROS2 + DDS introspection in one product risks bloating the tool surface beyond what a focused MCP should expose. **Re-scope decision (2026-07-08, ratified retroactively).** The register's original ceiling — 5 ROS2 tools + at most 3 DDS tools, any 9th tool gated on a re-scope discussion documented *here* before code lands — was crossed during v0.4.0 **without that discussion being recorded in this register**, a governance gap surfaced by the 2026-07-08 external audit. The three tools that broke it are deliberate and were acknowledged in the CHANGELOG and `docs/projet-file/mcp-02-spec.md §2` at ship time: `participant_events` (9th, v0.4.0 Phase 1), `topic_metrics` (10th, Phase 2), `peek_bag_samples` (11th, Phase 3). They are accepted; the revised ceiling is **11 tools**. A 12th tool now needs an explicit re-scope discussion documented in this register before code lands. Mitigation going forward: the `verify-change` skill's doc-drift step and the `docs-curator` sweep keep this register, `README.md`, and `CLAUDE.md` in sync so a ceiling break cannot ship undocumented again.
175
175
 
176
176
  ---
177
177
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "topicforge"
7
- version = "0.5.0"
7
+ version = "0.5.2"
8
8
  description = "ROS Topic Inspector & Bag Analyzer MCP server for AI agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -22,14 +22,20 @@ classifiers = [
22
22
  "Programming Language :: Python :: 3.13",
23
23
  ]
24
24
  dependencies = [
25
- "mcp>=1.0.0",
25
+ "mcp>=1.0.0,<2",
26
26
  "pydantic>=2.6",
27
27
  ]
28
28
 
29
29
  [project.optional-dependencies]
30
30
  dev = [
31
31
  "pytest>=8.0",
32
+ "pytest-cov>=5.0",
32
33
  "ruff>=0.4",
34
+ # rosbags (Apache-2.0, pure-Python) is a *dev* dependency so the
35
+ # bag-analysis I/O tests actually run in CI instead of self-skipping on
36
+ # `requires_rosbags`. It is NOT a runtime dependency — end users opt in
37
+ # via `topicforge[bags]`.
38
+ "rosbags>=0.9",
33
39
  ]
34
40
  # v0.3.0: split DDS extras per vendor. `[dds]` (the union) pulls both
35
41
  # OSS Python participants ; granular `[dds-cyclone]` / `[dds-fast]`
@@ -107,6 +113,33 @@ markers = [
107
113
  "requires_rosbags: tests needing the `rosbags` Python library; auto-skip without it (v0.4.0 Phase 3+)",
108
114
  ]
109
115
 
116
+ [tool.coverage.run]
117
+ branch = true
118
+ source = ["topicforge"]
119
+ omit = [
120
+ # The two real DDS adapters import their vendor binding (cyclonedds /
121
+ # fastdds) at module top level, so they are structurally unreachable
122
+ # without those SDKs installed. Their pure logic was extracted into
123
+ # adapters/common/{dds_introspection,qos_normalize,cdr_decoder}.py
124
+ # (Lot 0) which IS measured here; the thin binding shells are exercised
125
+ # only under the `-m integration` real-bus tier. Excluded so the
126
+ # coverage gate reflects testable code rather than being dragged down
127
+ # by lines no unit run can reach.
128
+ "*/adapters/dds_cyclone/adapter.py",
129
+ "*/adapters/dds_fast/adapter.py",
130
+ ]
131
+
132
+ [tool.coverage.report]
133
+ show_missing = true
134
+ skip_covered = false
135
+ # Threshold reflects the unit-testable surface (the two binding shells are
136
+ # omitted above). Baseline at the Lot 0 achieved floor (87% total); the
137
+ # extracted pure modules (qos_normalize, dds_introspection) are ~100%, while
138
+ # factory.py / bag_service.py binding-present branches stay uncovered without
139
+ # the SDKs installed. Ratchet upward as those get tests; do not lower without
140
+ # a note in the changelog.
141
+ fail_under = 85
142
+
110
143
  [tool.ruff]
111
144
  line-length = 100
112
145
  target-version = "py311"
@@ -1,5 +1,5 @@
1
1
  """TopicForge — ROS Topic Inspector & Bag Analyzer MCP server."""
2
2
 
3
- __version__ = "0.5.0"
3
+ __version__ = "0.5.2"
4
4
 
5
5
  __all__ = ["__version__"]