topicforge 0.3.0__tar.gz → 0.4.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 (104) hide show
  1. topicforge-0.4.0/CHANGELOG.md +562 -0
  2. {topicforge-0.3.0 → topicforge-0.4.0}/PKG-INFO +25 -5
  3. {topicforge-0.3.0 → topicforge-0.4.0}/README.md +13 -4
  4. topicforge-0.4.0/docs/pro.md +115 -0
  5. {topicforge-0.3.0 → topicforge-0.4.0}/pyproject.toml +35 -1
  6. topicforge-0.4.0/scripts/integration/README.md +126 -0
  7. {topicforge-0.3.0 → topicforge-0.4.0}/src/topicforge/__init__.py +1 -1
  8. {topicforge-0.3.0 → topicforge-0.4.0}/src/topicforge/adapters/base.py +38 -3
  9. topicforge-0.4.0/src/topicforge/adapters/common/__init__.py +52 -0
  10. topicforge-0.4.0/src/topicforge/adapters/common/cdr_decoder.py +186 -0
  11. topicforge-0.4.0/src/topicforge/adapters/common/lifecycle.py +251 -0
  12. topicforge-0.4.0/src/topicforge/adapters/common/metrics_buffer.py +257 -0
  13. topicforge-0.4.0/src/topicforge/adapters/common/xtypes.py +117 -0
  14. topicforge-0.4.0/src/topicforge/adapters/composite.py +102 -0
  15. topicforge-0.4.0/src/topicforge/adapters/dds_cyclone/adapter.py +720 -0
  16. topicforge-0.4.0/src/topicforge/adapters/dds_dust/__init__.py +12 -0
  17. topicforge-0.4.0/src/topicforge/adapters/dds_dust/adapter.py +97 -0
  18. {topicforge-0.3.0 → topicforge-0.4.0}/src/topicforge/adapters/dds_fast/adapter.py +224 -26
  19. topicforge-0.4.0/src/topicforge/adapters/dds_opendds/__init__.py +15 -0
  20. topicforge-0.4.0/src/topicforge/adapters/dds_opendds/adapter.py +103 -0
  21. {topicforge-0.3.0 → topicforge-0.4.0}/src/topicforge/adapters/ros2_live/adapter.py +25 -0
  22. {topicforge-0.3.0 → topicforge-0.4.0}/src/topicforge/adapters/ros2_mock/adapter.py +34 -0
  23. topicforge-0.4.0/src/topicforge/adapters/ros2_mock/fixtures.py +542 -0
  24. {topicforge-0.3.0 → topicforge-0.4.0}/src/topicforge/config/settings.py +116 -21
  25. {topicforge-0.3.0 → topicforge-0.4.0}/src/topicforge/models/__init__.py +4 -0
  26. {topicforge-0.3.0 → topicforge-0.4.0}/src/topicforge/models/schemas.py +304 -8
  27. {topicforge-0.3.0 → topicforge-0.4.0}/src/topicforge/server/app.py +2 -1
  28. topicforge-0.4.0/src/topicforge/services/bag_service.py +276 -0
  29. topicforge-0.4.0/src/topicforge/services/factory.py +265 -0
  30. topicforge-0.4.0/src/topicforge/services/health.py +96 -0
  31. {topicforge-0.3.0 → topicforge-0.4.0}/src/topicforge/services/inspector.py +52 -0
  32. {topicforge-0.3.0 → topicforge-0.4.0}/src/topicforge/tools/handlers.py +171 -14
  33. topicforge-0.4.0/tests/integration/__init__.py +16 -0
  34. topicforge-0.4.0/tests/integration/conftest.py +58 -0
  35. topicforge-0.4.0/tests/integration/scenarios/lifecycle_tracking.json +34 -0
  36. topicforge-0.4.0/tests/integration/scenarios/multi_vendor_basic.json +39 -0
  37. topicforge-0.4.0/tests/integration/scenarios/qos_mismatch_detection.json +47 -0
  38. topicforge-0.4.0/tests/integration/scenarios/topic_metrics_frequency.json +30 -0
  39. topicforge-0.4.0/tests/integration/scenarios/topic_metrics_sequence_gaps.json +34 -0
  40. topicforge-0.4.0/tests/integration/scenarios/xtypes_decode.json +29 -0
  41. topicforge-0.4.0/tests/integration/test_real_bus.py +81 -0
  42. topicforge-0.4.0/tests/integration/test_scenarios_schema.py +139 -0
  43. topicforge-0.4.0/tests/test_analyze_bag_multi_format.py +110 -0
  44. topicforge-0.4.0/tests/test_bag_service.py +134 -0
  45. topicforge-0.4.0/tests/test_cdr_decoder.py +214 -0
  46. topicforge-0.4.0/tests/test_composite_adapter.py +333 -0
  47. {topicforge-0.3.0 → topicforge-0.4.0}/tests/test_config.py +136 -2
  48. topicforge-0.4.0/tests/test_dust_adapter.py +49 -0
  49. topicforge-0.4.0/tests/test_factory.py +325 -0
  50. topicforge-0.4.0/tests/test_health.py +267 -0
  51. {topicforge-0.3.0 → topicforge-0.4.0}/tests/test_inspector.py +36 -0
  52. topicforge-0.4.0/tests/test_lifecycle_buffer.py +218 -0
  53. topicforge-0.4.0/tests/test_metrics_buffer.py +308 -0
  54. {topicforge-0.3.0 → topicforge-0.4.0}/tests/test_mock_adapter.py +100 -0
  55. topicforge-0.4.0/tests/test_opendds_adapter.py +58 -0
  56. topicforge-0.4.0/tests/test_peek_bag_samples.py +91 -0
  57. topicforge-0.4.0/tests/test_pro_hook.py +83 -0
  58. {topicforge-0.3.0 → topicforge-0.4.0}/tests/test_tools_integration.py +10 -0
  59. topicforge-0.4.0/tests/test_topic_metrics.py +105 -0
  60. topicforge-0.4.0/tests/test_xtypes.py +77 -0
  61. topicforge-0.3.0/CHANGELOG.md +0 -154
  62. topicforge-0.3.0/docs/pro.md +0 -80
  63. topicforge-0.3.0/src/topicforge/adapters/common/__init__.py +0 -17
  64. topicforge-0.3.0/src/topicforge/adapters/dds_cyclone/adapter.py +0 -384
  65. topicforge-0.3.0/src/topicforge/adapters/ros2_mock/fixtures.py +0 -298
  66. topicforge-0.3.0/src/topicforge/services/factory.py +0 -129
  67. topicforge-0.3.0/src/topicforge/services/health.py +0 -65
  68. topicforge-0.3.0/tests/test_health.py +0 -126
  69. {topicforge-0.3.0 → topicforge-0.4.0}/.gitignore +0 -0
  70. {topicforge-0.3.0 → topicforge-0.4.0}/LICENSE +0 -0
  71. {topicforge-0.3.0 → topicforge-0.4.0}/docs/DDS_QUICKSTART.md +0 -0
  72. {topicforge-0.3.0 → topicforge-0.4.0}/docs/MIGRATION_v0.1_to_v0.2.md +0 -0
  73. {topicforge-0.3.0 → topicforge-0.4.0}/docs/MIGRATION_v0.2_to_v0.3.md +0 -0
  74. {topicforge-0.3.0 → topicforge-0.4.0}/docs/TESTING.md +0 -0
  75. {topicforge-0.3.0 → topicforge-0.4.0}/docs/dds-interop-matrix.md +0 -0
  76. {topicforge-0.3.0 → topicforge-0.4.0}/docs/product-plan.md +0 -0
  77. {topicforge-0.3.0 → topicforge-0.4.0}/src/topicforge/__main__.py +0 -0
  78. {topicforge-0.3.0 → topicforge-0.4.0}/src/topicforge/adapters/__init__.py +0 -0
  79. {topicforge-0.3.0 → topicforge-0.4.0}/src/topicforge/adapters/common/dds_helpers.py +0 -0
  80. {topicforge-0.3.0 → topicforge-0.4.0}/src/topicforge/adapters/common/qos_analyzer.py +0 -0
  81. {topicforge-0.3.0 → topicforge-0.4.0}/src/topicforge/adapters/dds_cyclone/__init__.py +0 -0
  82. {topicforge-0.3.0 → topicforge-0.4.0}/src/topicforge/adapters/dds_fast/__init__.py +0 -0
  83. {topicforge-0.3.0 → topicforge-0.4.0}/src/topicforge/adapters/ros2_live/__init__.py +0 -0
  84. {topicforge-0.3.0 → topicforge-0.4.0}/src/topicforge/adapters/ros2_mock/__init__.py +0 -0
  85. {topicforge-0.3.0 → topicforge-0.4.0}/src/topicforge/config/__init__.py +0 -0
  86. {topicforge-0.3.0 → topicforge-0.4.0}/src/topicforge/server/__init__.py +0 -0
  87. {topicforge-0.3.0 → topicforge-0.4.0}/src/topicforge/services/__init__.py +0 -0
  88. {topicforge-0.3.0 → topicforge-0.4.0}/src/topicforge/services/constants.py +0 -0
  89. {topicforge-0.3.0 → topicforge-0.4.0}/src/topicforge/telemetry/__init__.py +0 -0
  90. {topicforge-0.3.0 → topicforge-0.4.0}/src/topicforge/telemetry/client.py +0 -0
  91. {topicforge-0.3.0 → topicforge-0.4.0}/src/topicforge/tools/__init__.py +0 -0
  92. {topicforge-0.3.0 → topicforge-0.4.0}/tests/__init__.py +0 -0
  93. {topicforge-0.3.0 → topicforge-0.4.0}/tests/conftest.py +0 -0
  94. {topicforge-0.3.0 → topicforge-0.4.0}/tests/fixtures/csv_echo_imu.txt +0 -0
  95. {topicforge-0.3.0 → topicforge-0.4.0}/tests/fixtures/csv_echo_pose_multi.txt +0 -0
  96. {topicforge-0.3.0 → topicforge-0.4.0}/tests/test_cyclone_adapter.py +0 -0
  97. {topicforge-0.3.0 → topicforge-0.4.0}/tests/test_dds_cross_vendor.py +0 -0
  98. {topicforge-0.3.0 → topicforge-0.4.0}/tests/test_dds_helpers.py +0 -0
  99. {topicforge-0.3.0 → topicforge-0.4.0}/tests/test_dds_schemas.py +0 -0
  100. {topicforge-0.3.0 → topicforge-0.4.0}/tests/test_fast_adapter.py +0 -0
  101. {topicforge-0.3.0 → topicforge-0.4.0}/tests/test_live_adapter_parse.py +0 -0
  102. {topicforge-0.3.0 → topicforge-0.4.0}/tests/test_live_adapter_subprocess.py +0 -0
  103. {topicforge-0.3.0 → topicforge-0.4.0}/tests/test_qos_analyzer.py +0 -0
  104. {topicforge-0.3.0 → topicforge-0.4.0}/tests/test_telemetry.py +0 -0
@@ -0,0 +1,562 @@
1
+ # Changelog
2
+
3
+ All notable changes to TopicForge are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.4.0]
9
+
10
+ ### Sprint v0.4.0 — Phase 3 (bag analysis multi-format)
11
+
12
+ > Branch `feat/v0.4.0-phase3-bag-analysis-mcap-db3-rosbag`. Three
13
+ > sub-milestones (3.1 CDR refactor, 3.2 bag_service + enriched
14
+ > analyze_bag, 3.3 peek_bag_samples). v0.3.0 stays the live PyPI
15
+ > version ; no version bump in this branch — manual maintainer step
16
+ > after final review.
17
+
18
+ #### Refactored (sub-milestone 3.1 — CDR decoder commun)
19
+
20
+ - **6 vendor-agnostic helpers extracted** from `dds_cyclone/adapter.py`
21
+ into `adapters/common/cdr_decoder.py`: `decode_dynamic_sample`,
22
+ `iter_field_names`, `decode_field_value`, `dynamic_type_name`,
23
+ `extract_seq_from_payload`, `extract_publish_ns_from_payload`. The
24
+ Cyclone module keeps `_underscore` aliases pointing at the new
25
+ common functions, so every pre-Phase-3 call site keeps working
26
+ without rewrites.
27
+ - **22 new pure-logic tests** (`tests/test_cdr_decoder.py`) pin the
28
+ extracted contract in isolation. The 22 Phase 1.5 XTypes Cyclone
29
+ tests (gated by `requires_cyclonedds`) remain green through the
30
+ full pipeline on hosts with the SDK installed — refactor is
31
+ transparent.
32
+
33
+ #### Added (sub-milestone 3.2 — BagService + enriched analyze_bag)
34
+
35
+ - **`BagService`** (`services/bag_service.py`) — facade wrapping the
36
+ `rosbags` library (Apache 2.0, pure-Python). Two methods:
37
+ `analyze(path)` (stats + format detection) and `peek_samples(path,
38
+ topic, count)` (decoded samples via the shared cdr_decoder).
39
+ Lazy import of rosbags ; methods raise a clear AdapterError when
40
+ the library is absent (caller can fall back to v0.3.0 text-parse
41
+ behavior on the analyze path ; sample peek requires the library).
42
+ - **`BagAnalysis` schema enriched** (`models/schemas.py`) with four
43
+ **additive optional fields**:
44
+ - `bag_format: Literal["mcap","db3","bag","unknown"] | None` —
45
+ container format detected from the file extension
46
+ - `samples_decoded_count: int` — total decoded sample count
47
+ (analyze keeps this at 0 ; peek_bag_samples does the decoding)
48
+ - `recording_duration_ns: int | None` — recording duration in ns
49
+ from the bag's index when readable
50
+ - `participants_recorded: list[ParticipantInfo]` — DDS participants
51
+ embedded in the bag container (MCAP can ; .db3 / .bag generally
52
+ don't — empty list is the common case)
53
+ - **`MOCK_BAG_ANALYSIS` updated** with deterministic enriched values.
54
+ New `MOCK_BAG_SAMPLES` dict + `mock_bag_samples_for(topic, count)`
55
+ helper for the upcoming peek_bag_samples tool.
56
+ - **`[bags]` pyproject extra** (`rosbags>=0.9`) — NOT bundled in
57
+ `[all]` for granular install. New `requires_rosbags` pytest
58
+ marker.
59
+
60
+ #### Added (sub-milestone 3.3 — peek_bag_samples MCP tool)
61
+
62
+ - **`peek_bag_samples(path, topic, count) -> SampleResult`** — the
63
+ 11th MCP tool, third explicit ceiling break. Returns up to
64
+ `count` decoded samples for `topic` from a recorded bag file.
65
+ Distinct from `peek_dds_samples` (live bus) and `sample_messages`
66
+ (ROS2 graph live peek) — this tool is **post-mortem inspection**.
67
+ Same `SampleResult` shape across all three tools so LLM
68
+ consumers read one envelope. Each sample's `_decode_status`
69
+ annotation carries over from the shared cdr_decoder.
70
+ - **`MiddlewareAdapter` protocol** gains
71
+ `peek_bag_samples(path, topic, count) -> SampleResult`. All
72
+ existing adapters implement it: Mock via fixtures, Ros2CliAdapter
73
+ via `BagService`, Cyclone / Fast / OpenDDS / Dust raise their
74
+ existing roadmap errors (DDS-only adapters don't do bag analysis).
75
+ - **CompositeAdapter** delegates `peek_bag_samples` to the ROS half
76
+ (bag analysis is ROS-native — MCAP is the canonical ROS2
77
+ recording format).
78
+ - **10 new tests** in `tests/test_peek_bag_samples.py` (tool-level
79
+ via Inspector + MockAdapter). Plus extensions to
80
+ `test_composite_adapter`, `test_factory`, `test_opendds_adapter`,
81
+ `test_dust_adapter`, `test_tools_integration` (MVP_TOOLS grows
82
+ from 10 → 11).
83
+ - **`docs/projet-file/mcp-02-spec.md` §2** — ceiling note updated
84
+ to 11 tools.
85
+
86
+ #### Notes (Phase 3)
87
+
88
+ - **Bag analysis is offline-only.** `analyze_bag` and
89
+ `peek_bag_samples` read files ; they do not introspect the live
90
+ bus. For live introspection use the existing tool set
91
+ (`list_topics`, `peek_dds_samples`, etc.).
92
+ - **rosbags requirement.** `BagService.peek_samples` strictly requires
93
+ `pip install topicforge[bags]` — no silent fallback for sample
94
+ peek. `analyze_bag` retains the v0.3.0 `ros2 bag info` text-parse
95
+ fallback on Ros2CliAdapter when rosbags is absent ; the enriched
96
+ fields populate at their safe defaults in that path.
97
+ - **Backward compat.** Every v0.3.0 / Phase 1 / 1.5 / 2 test passes
98
+ unchanged. BagAnalysis additive fields preserve the wire contract
99
+ for v0.3.0 consumers ignoring them.
100
+ - **The version bump and tag are deliberate manual maintainer
101
+ steps** after Phase 3 review. This branch leaves `pyproject.toml`
102
+ at `0.3.0`, `__version__` at `"0.3.0"`, and the
103
+ `## [Unreleased]` heading intact.
104
+
105
+ ### Sprint v0.4.0 — Phase 2 (temporal metrics + real-bus rig)
106
+
107
+ > Branch `feat/v0.4.0-phase2-metrics-and-realbus-testing`. Two
108
+ > sub-milestones (2.1 metrics, 2.2 real-bus rig). v0.3.0 stays the
109
+ > live PyPI version ; no version bump.
110
+
111
+ #### Added (sub-milestone 2.1 — topic_metrics)
112
+
113
+ - **`topic_metrics(topic, window_seconds, domain_id)` MCP tool**
114
+ (the 10th — second explicit ceiling break after `participant_events`
115
+ in Phase 1). Returns a `TopicMetrics` payload with
116
+ `samples_observed`, `frequency_hz_observed` (and `_declared` from
117
+ QoS Deadline when known), `sequence_gaps_count`, `latency_ns_p50` /
118
+ `_p95` / `_p99`, and boolean availability flags for each
119
+ conditional metric. Window range: 1..3600 seconds, default 60.
120
+ - **`MetricsBuffer`** (`adapters/common/metrics_buffer.py`) — pure-
121
+ Python logic, RLock-protected per-topic deque with
122
+ `MAX_SAMPLES_PER_TOPIC=1000` drop-oldest cap. Pure-Python percentile
123
+ computation (no NumPy dependency added). Sequence gap counting
124
+ tolerates out-of-order arrivals and dedupes duplicates.
125
+ - **`TopicMetrics`** Pydantic schema (`models/schemas.py`) — frozen,
126
+ `extra="forbid"`, every field documented with the
127
+ None/zero-on-unavailable semantics that surfaces partial data
128
+ cleanly to LLM callers.
129
+ - **Cyclone + Fast adapter integration** — `_peek_builtin` and
130
+ `_peek_user_topic` paths now call `self._metrics.record(...)` for
131
+ each sample they surface. Sequence number and publish timestamp
132
+ extracted best-effort from the decoded payload via two new helpers
133
+ in `dds_cyclone/adapter.py`. **Opportunistic fill caveat** —
134
+ neither `cyclonedds` nor `fastdds` 2.6.x Python bindings expose
135
+ at-sample-receive callbacks, so the metrics buffer accumulates
136
+ ONLY as `peek_dds_samples` is exercised. Tool description surfaces
137
+ this to the LLM.
138
+ - **Mock fixture** (`/dds/heartbeat_10hz`, 100 samples spaced 100 ms
139
+ apart with synthetic 50 ms latency and sequence 0..99) plus a
140
+ singleton topic and a cross-domain topic for filter testing.
141
+ - **~30 new tests**: `tests/test_metrics_buffer.py` (~22 pure-logic
142
+ tests covering the helpers, ring overflow, multi-topic isolation,
143
+ domain filtering, thread-safety smoke) + `tests/test_topic_metrics.py`
144
+ (~12 tool-level tests via Inspector + MockAdapter).
145
+
146
+ #### Changed (sub-milestone 2.1)
147
+
148
+ - **`MiddlewareAdapter` protocol** gains `topic_metrics(topic,
149
+ window_seconds, domain_id)`. All existing adapters implement it:
150
+ Cyclone + Fast via the new buffer, Mock via fixtures, Ros2CliAdapter
151
+ / OpenDDS stub / Dust stub raise their existing roadmap errors.
152
+ - **`AdapterName` Literal** unchanged (no new vendor) ; `MVP_TOOLS`
153
+ test set grows from 9 to 10.
154
+
155
+ #### Added (sub-milestone 2.2 — real-bus rig)
156
+
157
+ - **`tests/integration/`** — six scenario JSON files exercising
158
+ the multi-vendor OMG-DDS-RTPS claim against real publishers:
159
+ `multi_vendor_basic`, `lifecycle_tracking`, `qos_mismatch_detection`,
160
+ `xtypes_decode`, `topic_metrics_frequency`,
161
+ `topic_metrics_sequence_gaps`. Each scenario declares
162
+ `required_vendors` so the runner can skip cleanly when a binding
163
+ is missing.
164
+ - **`tests/integration/test_scenarios_schema.py`** — pure-Python
165
+ validation of every scenario's structure. Runs in default
166
+ `make check` (8 tests). No SDK, no Docker required.
167
+ - **`tests/integration/test_real_bus.py`** — parametrized integration
168
+ tests, marked `@pytest.mark.integration` ; **gated out of the
169
+ default pytest invocation**. Runs only with `pytest -m integration`
170
+ or the labeled CI workflow.
171
+ - **`scripts/integration/scenarios_runner.py`** — Python entry point
172
+ that probes locally installed DDS bindings, dispatches scenarios,
173
+ and reports pass/fail per assertion. Pragmatic partial-run: missing
174
+ vendors are skipped with a clear `[skipped]` log rather than failing
175
+ the whole batch (D6).
176
+ - **`scripts/integration/publishers/`** — per-vendor minimal publishers
177
+ (`cyclone_publisher.py`, `fast_publisher.py`, `opendds_publisher.py`).
178
+ Symmetric CLI surface: `--topic`, `--rate-hz`, `--duration-s`,
179
+ `--domain`, `--gap-at-seq`. OpenDDS publisher is a documented
180
+ scaffold (no PyPI binding yet).
181
+ - **`scripts/integration/run-local.{sh,ps1}`** — standalone entry
182
+ points for Linux/macOS and Windows. No Docker required.
183
+ - **`scripts/integration/docker-compose.yml` + per-vendor Dockerfiles** —
184
+ full multi-vendor rig for CI runs and maintainer validation.
185
+ Cyclone + Fast images build against the OSS PyPI bindings ; OpenDDS
186
+ image documents the BYO path. `topicforge` observer image installs
187
+ from repo source plus `[dds]` extra.
188
+ - **`.github/workflows/integration.yml`** — manual label trigger
189
+ (`integration-tests`). Runs schema validation, builds the compose
190
+ stack, sleeps 30 s for discovery, runs `pytest -m integration`,
191
+ tears down. Default `ci.yml` is untouched.
192
+ - **New pytest marker** `integration` — added to `pyproject.toml`
193
+ alongside the existing `requires_*` markers.
194
+
195
+ #### Notes (sub-milestone 2.2)
196
+
197
+ - **Validation reality.** The OSS-CI default pipeline lint-validates
198
+ scenario JSON structure, YAML/PowerShell/bash syntax, and the
199
+ runner's dispatch logic. It does NOT pull / build / run Docker
200
+ images. The maintainer validates the live publisher path locally
201
+ before merging Phase 2.2 ; the integration CI workflow is the
202
+ shared validation surface once an SDK-rich runner host is
203
+ available.
204
+ - **OpenDDS publisher is a scaffold.** `pyopendds` is not on PyPI as
205
+ of 2026-05-14 — the publisher script exits with an actionable
206
+ error message, and the scenarios runner skips OpenDDS scenarios
207
+ cleanly. Scenarios requiring OpenDDS (`multi_vendor_basic`) still
208
+ run partial coverage of the other vendors.
209
+ - **Real-bus assertion evaluation is structural at Phase 2.2.** The
210
+ runner dispatches scenarios and reports per-assertion status, but
211
+ the full per-assertion verification logic (parsing TopicForge tool
212
+ outputs, comparing against scenario `expect` clauses) is the
213
+ maintainer's follow-up. The scaffold is ready to receive that
214
+ wiring without re-touching the surrounding files.
215
+
216
+ ### Sprint v0.4.0 — Phase 1 (DDS observability core)
217
+
218
+ > Internal-only ; the branch `feat/v0.4.0-phase1-observability-core` is
219
+ > merging to `main` between v0.3.0 and v0.4.0. **No version bump in
220
+ > this section** — `pyproject.toml`/`__version__` stay at `0.3.0` until
221
+ > Phase 3 closes.
222
+
223
+ #### Added
224
+
225
+ - **`CompositeAdapter` (`adapters/composite.py`).** New wrapper that
226
+ routes the 4 ROS2 protocol methods to a `Ros2CliAdapter` and the 3
227
+ DDS methods (+ `participant_events`) to a DDS adapter, so a single
228
+ process serves all 9 tools when `TOPICFORGE_MODE=live` and
229
+ `TOPICFORGE_DDS_BACKEND=cyclone|fast` are configured together. The
230
+ `name` collapses to `"ros2_cli+cyclone"` or `"ros2_cli+fast"` ;
231
+ `effective_mode` reports `"live"` when either half is live.
232
+ - **Participant lifecycle tracking.** `ParticipantInfo` gains four
233
+ additive optional fields: `first_seen_ns`, `last_seen_ns`, `status`
234
+ (`active`/`left`/`unknown`), `seen_count`. v0.3.0 producers and
235
+ fixtures remain valid because every new field has a safe default.
236
+ - **`LifecycleBuffer`** (`adapters/common/lifecycle.py`) — RLock-protected
237
+ ring buffer (cap 200 events, drop-oldest) shared by Cyclone (polling
238
+ reconciliation) and Fast DDS (listener callbacks). Pure logic, no DDS
239
+ dependency ; testable without any SDK installed.
240
+ - **`participant_events` MCP tool (the 9th).** New read-only tool that
241
+ returns DDS participant `discovered`/`lost` events over a configurable
242
+ window (default 300s, range 1..86400, hard cap 200 events, newest
243
+ first). Breaks the 8-tool ceiling documented in
244
+ `docs/projet-file/mcp-02-spec.md §2` ; acknowledged in this Phase 1
245
+ scope.
246
+ - **`HealthReport.ros_backend`** (`Literal["mock","ros2_cli","none"]`,
247
+ default `"none"`). Symmetric to the existing `dds_backend` ; lets
248
+ clients distinguish the ROS and DDS halves of a composed runtime.
249
+
250
+ #### Changed
251
+
252
+ - **Factory decision tree** (`services/factory.py`). Live mode now
253
+ attempts to build both a ROS2 CLI adapter and a DDS adapter and
254
+ wraps them in a `CompositeAdapter` when both succeed. Graceful
255
+ degradation paths preserved: DDS missing → ROS2 CLI alone (v0.3.0
256
+ behavior) ; ROS2 CLI missing → DDS-only adapter ; neither
257
+ available → MockAdapter.
258
+ - **`MiddlewareAdapter` protocol** (`adapters/base.py`). Gains
259
+ `participant_events(domain_id, lookback_seconds)`. All adapters
260
+ implement it: Mock returns deterministic fixtures, Cyclone and Fast
261
+ read from their `LifecycleBuffer`, Ros2CliAdapter raises
262
+ `AdapterError(_DDS_MODULE_INACTIVE_MSG)`. `AdapterName` Literal
263
+ widened with the two composite tags.
264
+ - **`CycloneDdsAdapter.list_participants`** now feeds the
265
+ `LifecycleBuffer` (polling delta reconciliation per call) and
266
+ returns enriched `ParticipantInfo` snapshots with lifecycle fields.
267
+ Discovery-sample mapping unchanged ; the returned shape is a
268
+ superset of v0.3.0.
269
+ - **`FastDdsAdapter._DiscoveryListener`** now feeds the
270
+ `LifecycleBuffer` from `on_participant_discovery` callbacks
271
+ (arrival AND removal events captured natively, no polling
272
+ reconciliation needed).
273
+
274
+ #### Notes
275
+
276
+ - **Cyclone lifecycle caveat.** Cyclone's lifecycle log is updated only
277
+ when `list_participants` (or any internal poll of the
278
+ `DCPSParticipant` builtin reader) is called. A participant that
279
+ joined and left between two tool calls is invisible. The
280
+ `participant_events` tool description makes this explicit.
281
+ - **Backward compatibility.** Zero v0.3.0 tests regress. Pydantic
282
+ `extra="forbid"` is preserved on every model ; the four new
283
+ `ParticipantInfo` fields have safe defaults so producers built
284
+ against v0.3.0 schemas keep working. `ParticipantEvent` is a new
285
+ model — clients ignoring it continue to work.
286
+
287
+ #### Added (continued — sub-milestone 1.3)
288
+
289
+ - **`peek_dds_samples` on user-defined topics.** v0.3.0 raised an
290
+ `AdapterError` pointing at the v0.3.x roadmap for any non-builtin
291
+ topic ; v0.4.0 Phase 1 returns best-effort decoded samples. The
292
+ payload may carry three reserved keys:
293
+ - `_decode_status`: `"full"` (every IDL field decoded) /
294
+ `"partial"` (some fields decoded, others opaque) /
295
+ `"raw"` (binding could not resolve the dynamic XTypes — bytes
296
+ preserved as hex).
297
+ - `_decode_note`: short diagnostic explaining the non-`full` status.
298
+ - `_raw_bytes_hex`: hex-encoded serialized payload preview (capped
299
+ at 4096 hex chars ; `_raw_bytes_truncated=True` flags clipping).
300
+ - **`adapters/common/xtypes.py`** — adapter-agnostic helpers
301
+ (`annotate_full`, `annotate_partial`, `annotate_raw`) so Cyclone and
302
+ Fast DDS produce identical wire output regardless of binding
303
+ capabilities. Pure logic, testable without any SDK.
304
+ - **Mock fixtures**: two new user-topic exemplars
305
+ (`/dds/ddsforge/example` returns `_decode_status="full"` ;
306
+ `/dds/ddsforge/opaque` returns `_decode_status="raw"`) so the
307
+ payload shape can be exercised end-to-end without a DDS bus.
308
+
309
+ #### Changed (continued — sub-milestone 1.3)
310
+
311
+ - **`CycloneDdsAdapter.peek_dds_samples`** — user topics now go
312
+ through `_peek_user_topic`. The path probes builtin DCPS
313
+ Subscription / Publication readers to confirm the topic is on the
314
+ bus (raises `AdapterError` if not), then attempts
315
+ `cyclonedds.dynamic` dynamic decode ; on failure (most cases at
316
+ v0.4.0 Phase 1 — full decode lands in a Phase 1+ patch) returns a
317
+ single annotated raw-bytes sample with `_decode_status="raw"`.
318
+ - **`FastDdsAdapter.peek_dds_samples`** — symmetric shape via
319
+ `_peek_user_topic`. Fast DDS 2.6.x dynamic XTypes is partial, so
320
+ the raw-bytes fallback is the common path ; the wire shape is
321
+ identical to Cyclone's. Same `AdapterError` on unknown topic.
322
+ - **`peek_dds_samples` tool description** updated to remove the stale
323
+ `"v0.2.0 stub"` wording and document the user-topic decoding story
324
+ (full / partial / raw status, `_raw_bytes_hex` shape, binding
325
+ caveats per backend).
326
+
327
+ ### Sprint v0.4.0 — Phase 1.5 (auto-detect + OSS expansion + Pro framing)
328
+
329
+ > Same `[Unreleased]` line. Phase 1.5 is the bridge between Phase 1 (DDS
330
+ > observability core) and Phase 2 (Pro tier launch). Branch
331
+ > `feat/v0.4.0-phase15-auto-detect-and-oss-expansion`. No version bump.
332
+
333
+ #### Added (sub-milestone 1.5.1 — auto-detect + XTypes push)
334
+
335
+ - **8-vendor DDS auto-detect chain** in `Settings.effective_dds_backend`.
336
+ Priority order: `rti > opensplice > coredx > intercom (Pro) > opendds
337
+ > fast > cyclone > dust (OSS) > mock`. The chain probes each vendor's
338
+ Python module via `importlib.util.find_spec` and returns the first
339
+ hit. Pro vendors are probed against `topicforge_pro.adapters.<vendor>`
340
+ rather than the upstream SDK directly — the OSS core never imports a
341
+ commercial vendor binding.
342
+ - **`DdsBackend` / `ResolvedDdsBackend` Literals widened** with 5 new
343
+ vendor values: `opensplice`, `coredx`, `intercom`, `opendds`, `dust`.
344
+ - **`HealthReport.dds_backend` Literal widened** symmetrically. Soft-
345
+ breaking on the producer side ; strict JSON-schema clients pinned to
346
+ v0.3.0 will reject the new values unless their schema regenerates.
347
+ - **`AdapterName` Literal widened** with the 5 new vendor tags and 6
348
+ new composite tags (`ros2_cli+rti`, `ros2_cli+opensplice`, etc.).
349
+ - **Cyclone XTypes pipeline**. `_try_dynamic_decode_cyclone` no longer
350
+ always returns None — it probes `cyclonedds.dynamic` entry points,
351
+ resolves a type id via `DCPSPublication`, builds a typed reader, and
352
+ decodes samples field-by-field with per-construct granularity.
353
+ Fallback to `annotate_raw` when any step fails. Real-bus validation
354
+ awaits user feedback ; the v0.4.0 Phase 1 plumbing is now a
355
+ best-effort path rather than always-fallback.
356
+ - **Fast DDS `TypeObjectFactory` probe** in `_try_dynamic_decode_fast`.
357
+ The Fast DDS 2.6.x dynamic XTypes Python surface is incomplete, so
358
+ the probe currently returns None and the raw-bytes fallback fires ;
359
+ the structural change makes the v0.5 decode patch a small follow-up.
360
+
361
+ #### Added (sub-milestone 1.5.2 — OSS adapter expansion)
362
+
363
+ - **`OpenDdsAdapter`** (`adapters/dds_opendds/`) — stub. Probes
364
+ `pyopendds` via `find_spec` ; `is_available()` reports False when the
365
+ binding is absent (always, in 2026-05-14, since the package is not on
366
+ PyPI). All 8 protocol methods raise `AdapterError(_OPENDDS_ROADMAP_MSG)`.
367
+ - **`DustDdsAdapter`** (`adapters/dds_dust/`) — even thinner stub.
368
+ `is_available()` always False ; Dust DDS is Rust-native with no
369
+ maintained Python binding.
370
+ - **`[dds-opendds]` and `[dds-dust]` pyproject extras** as placeholder
371
+ pins (`pyopendds>=0.1`, `dust-dds-python>=0.1`). Neither package is
372
+ on PyPI today ; the extras anchor the auto-detect probe and will
373
+ resolve cleanly when upstream releases.
374
+ - **`[dds-all-oss]` union extra** — `topicforge[dds] + dds-opendds +
375
+ dds-dust` for users explicitly opting into the stubs.
376
+ - **New pytest markers** `requires_opendds`, `requires_dust` — auto-skip
377
+ when bindings are absent (same convention as `requires_cyclonedds`).
378
+
379
+ #### Added (sub-milestone 1.5.3 — pro/ scaffold + docs)
380
+
381
+ - **`tests/test_pro_hook.py`** — exercises `_try_register_pro` with a
382
+ fake `topicforge_pro` module injected via `sys.modules`. Three test
383
+ cases: package absent → False, package present + register succeeds →
384
+ True + side-effect captured, register raises → False + error logged.
385
+ Locks in the OSS-side contract that the Pro plugin must implement.
386
+ - **`docs/pro.md` rewritten** with the corrected tier framing: OSS =
387
+ community DDS adapters (Cyclone, Fast — OpenDDS / Dust stubs) + base
388
+ ROS2 introspection ; Pro = commercial DDS adapters (RTI Connext,
389
+ OpenSplice [legacy], CoreDX, InterCOM) + URDF / Bag Anomaly /
390
+ Multi-bag Diff diagnostics. Pricing terms preserved at $12/$19.
391
+
392
+ #### Changed (sub-milestone 1.5.3)
393
+
394
+ - **README.md install section** enriched. Documents the 8-vendor auto-
395
+ detect chain and the OSS/Pro tier split. Removed the stale "v0.3.0
396
+ limitation — single adapter at a time" — Phase 1 shipped the
397
+ composite adapter ; the README now describes the actual behavior.
398
+
399
+ #### Notes
400
+
401
+ - **Pro tier scaffold lives in `pro/` (gitignored).** The Phase 1.5
402
+ branch ships RtiConnextAdapter + OpenSplice stub + license skeleton
403
+ + register.py in the working tree under `pro/`, but `.gitignore`
404
+ excludes the folder entirely — none of those files are committed.
405
+ The maintainer copies them to a private backup repo before launching
406
+ the `topicforge-pro` PyPI package.
407
+ - **Backward compatibility**: zero v0.3.0 + Phase 1 test regressions.
408
+ `TOPICFORGE_DDS_BACKEND=mock | cyclone | fast | rti | auto` continue
409
+ to resolve as before ; the 5 new vendor values are additive.
410
+ - **No 10th MCP tool.** Phase 1.5 is structural / docs only — the
411
+ 9-tool surface (8 v0.3.0 + `participant_events` from Phase 1) is
412
+ preserved exactly.
413
+ - **Pyopendds and dust-dds-python pins are placeholders.** A user
414
+ running `pip install topicforge[dds-opendds]` today will see an
415
+ install failure ; this is expected and the extras exist to anchor
416
+ the auto-detect framework for the day the upstream packages ship.
417
+
418
+ ## [0.3.0] - 2026-05-14
419
+
420
+ ### Strategic
421
+
422
+ - **OMG DDS-RTPS multi-vendor positioning.** TopicForge is now framed as a read-only DDS-RTPS observer that joins the bus via one of two OSS Python participants — Eclipse CycloneDDS or eProsima Fast DDS — and observes every conformant vendor on the domain (RTI Connext, OpenDDS, CoreDX, Dust DDS in Rust, etc.) regardless of host language. See `docs/dds-interop-matrix.md` for the canonical statement and `docs/projet-file/references/omg-dds-interop-2025-05-08.xlsx` for the OMG May 2025 interop reference. The earlier v0.2.0/v0.3.0 phasing (Cyclone-only at v0.3.0, RTI at v0.3.0+) is collapsed: multi-vendor OSS lands together at v0.3.0 ; RTI Pro defers to v0.4.0+.
423
+
424
+ ### Added
425
+
426
+ - **Real `CycloneDdsAdapter`** — replaces the v0.2.0 stub with actual CycloneDDS discovery via `cyclonedds.builtin.BuiltinDataReader` on the DCPS participant/subscription/publication builtin topics. QoS extracted via `Policy.*` class-name introspection. `take_iter(timeout=...)` for bounded discovery. Lazy-imported via `services.factory`.
427
+ - **`FastDdsAdapter`** (`adapters/dds_fast/`) — new parallel OSS adapter built on the `fastdds` Python bindings (BSD-licensed, eProsima). Duck-typed listener subclass aggregates discovery callbacks under an RLock. Bounded `discovery_wait_ms=1500` warm-up after participant creation. `close()` releases the participant via `factory.delete_participant`.
428
+ - **`adapters/common/dds_helpers.py`** — vendor-neutral helpers: `canonicalize_vendor_id` (OMG vendor_id → canonical tag), `format_guid` (16-byte GUID → `xxxxxxxx.xxxxxxxx.xxxxxxxx.xxxxxxxx` canonical text), `DDS_ONLY_ERROR_MSG` (shared remediation message). Pure functions, no DDS dependency.
429
+ - **Pyproject extras refactor**: `[dds-cyclone]` (Cyclone only), `[dds-fast]` (Fast only), `[dds]` (both — union of v0.2.0 `[dds]` behavior plus fastdds).
430
+ - **`TOPICFORGE_DDS_BACKEND=fast`** — new accepted value alongside `mock | cyclone | rti | auto`.
431
+ - **3rd mock participant** with `vendor="fast"` exercises the multi-vendor positioning in mock mode.
432
+ - **New pytest marker** `requires_fastdds` — auto-skips without the binding.
433
+ - **35+ new tests**: `tests/test_dds_helpers.py`, `tests/test_fast_adapter.py`, `tests/test_dds_cross_vendor.py` (parametrized on both adapters), 6 analyzer edge cases in `tests/test_qos_analyzer.py`, 4 new health tests for DDS field population.
434
+
435
+ ### Changed
436
+
437
+ - **`ParticipantInfo.vendor` Literal widened** to include `"fast"`. **Strict JSON-schema clients pinned to v0.2.0 will reject `vendor:fast` unless their schema is regenerated.** Standard MCP clients reading tool descriptions dynamically are unaffected.
438
+ - **`HealthReport.dds_backend` Literal widened** to include `"fast"`. Same soft-breaking caveat.
439
+ - **`AdapterName` Literal widened** to include `"fast"` (internal type — no wire impact).
440
+ - **`DdsBackend` / `ResolvedDdsBackend`** widened to include `"fast"`.
441
+ - **`Settings.effective_dds_backend` auto resolution** now prefers Fast DDS > Cyclone DDS > Mock (was Cyclone > Mock in v0.2.0). v0.2.0 users with only `cyclonedds` installed see no change — Fast is unimportable on their host. Users with both SDKs installed will see Fast selected. Reflects the OMG May 2025 interop matrix.
442
+ - **`HealthService.report()`** now populates `dds_backend`, `dds_domain_id`, `middleware_available` — previously returned schema defaults regardless of configuration (v0.2.0 latent bug). `middleware_available` is checked via `importlib.util.find_spec` on the active backend's Python module.
443
+ - **`Ros2CliAdapter._DDS_MODULE_INACTIVE_MSG`** updated to mention both `pip install topicforge[dds-cyclone]` and `pip install topicforge[dds-fast]` remediation paths.
444
+ - **`Inspector` DDS topic validator relaxed** — `detect_qos_mismatches` and `peek_dds_samples` now accept DDS-native topic names (no leading `/` required, `::` separators allowed) via a new `_validate_topic_name_dds`. The strict ROS2 validator stays in place for the 5 ROS2 graph methods. Resolves audit-2026-05-14 "Refactor opportunities" #5.
445
+
446
+ ### Removed
447
+
448
+ - **`CycloneDdsAdapter` v0.2.0 stub** — `_NOT_IMPLEMENTED_MSG` and the corresponding `test_dds_surface_raises_stub_error_in_v020` test removed. The 3 DDS methods now serve real results when cyclonedds is installed.
449
+
450
+ ### Notes
451
+
452
+ - **OMG-DDS-RTPS interoperability** is the protocol guarantee that makes multi-vendor observation work — see `docs/dds-interop-matrix.md` and `docs/projet-file/references/omg-dds-interop-2025-05-08.xlsx`.
453
+ - **v0.3.0 `peek_dds_samples` limitation** — full-fidelity on the 4 builtin DCPS topics (`DCPSParticipant`, `DCPSSubscription`, `DCPSPublication`) ; arbitrary user topics raise an `AdapterError` with a v0.3.x roadmap pointer (XTypes/IDL discovery is the missing piece, both for Cyclone via `cyclonedds.dynamic.get_types_for_typeid` and for Fast DDS via XTypes remote type lookup).
454
+ - **`pip install topicforge[dds]` in v0.3.0** now pulls BOTH `cyclonedds` and `fastdds` (was Cyclone only in v0.2.0). Use `[dds-cyclone]` or `[dds-fast]` for single-vendor installs. See `docs/MIGRATION_v0.2_to_v0.3.md`.
455
+ - **Fast DDS pin**: `fastdds>=2.6.1,<3` — Fast DDS 3.x binding wheels for Python 3.11+ on Windows / Linux are not yet stable. Bump when upstream cuts stable 3.x wheels.
456
+ - **No code change to the 5 ROS2 tools** — `health_check`, `list_topics`, `get_topic_info`, `sample_messages`, `analyze_bag` behave identically to v0.2.0. The `mode_effective` wire contract is unchanged ; `health_check` now populates DDS fields correctly.
457
+ - **Full migration guide**: `docs/MIGRATION_v0.2_to_v0.3.md`.
458
+
459
+ ## [0.2.0] - 2026-05-14
460
+
461
+ ### Strategic
462
+
463
+ - **Mono-MCP pivot (2026-05-14).** The 3-to-5-MCP pack draft is collapsed into a 2-product strategy: TopicForge umbrella (this product — covers ROS2 today and grows a DDS observability module starting with v0.2.0), and **DatasetForge** (Vision Dataset Inspector, the standalone second product). The previously-planned standalone DDS-MCP product is cancelled — its spec is reframed as the TopicForge DDS module spec at `docs/projet-file/mcp-02-spec.md`. Motif: solo-maintenance cost of two parallel repos was the binding constraint, and ROS2 / DDS are the same problem shape (typed pub/sub graph introspection) under the same `MiddlewareAdapter` superset.
464
+
465
+ ### Added
466
+
467
+ - **DDS module — 3 new MCP tools.** `list_participants(domain_id)`, `detect_qos_mismatches(topic)`, `peek_dds_samples(topic, count)`. All read-only ; surface DDS-layer introspection distinct from the ROS2 graph tools. `peek_dds_samples` is deliberately separate from `sample_messages` — different layer, different semantics, distinct tool description so an LLM picks the right one in a mixed setup.
468
+ - **`MiddlewareAdapter` protocol** in `adapters/base.py` — superset of the historical `RosAdapter`. Covers both ROS2 graph methods and the new DDS methods under one contract. `RosAdapter` retained as a backward-compat alias (`RosAdapter = MiddlewareAdapter`).
469
+ - **`CycloneDdsAdapter`** (`adapters/dds_cyclone/`) — lazy-imported only when `TOPICFORGE_DDS_BACKEND=cyclone` and the optional `cyclonedds` extras are installed (`pip install topicforge[dds]`). **v0.2.0 ships a protocol-compliant stub**: the lazy import, `is_available()`, and routing all work ; the 3 DDS methods raise `AdapterError` with a v0.2.x roadmap pointer. The real CycloneDDS discovery (builtin topics, QoS pair extraction, typed reader for samples) lands in a v0.2.x patch. The mock backend (`TOPICFORGE_DDS_BACKEND=mock`, the default) exposes a working DDS surface against deterministic fixtures in the meantime.
470
+ - **3 new Pydantic schemas**: `QosProfile` (Reliability / Durability / History / Deadline at MVP), `ParticipantInfo` (GUID, vendor, hostname, domain_id), `MismatchReport` (incompatible_policies + severity). All frozen, `extra="forbid"`.
471
+ - **Pure analyzer** `adapters/common/qos_analyzer.detect_mismatches` — module-level pure function, testable against synthesized QoS pairs without any DDS middleware installed.
472
+ - **Environment variables**:
473
+ - `TOPICFORGE_DDS_BACKEND` — `mock | cyclone | rti | auto`, default `mock`. The DDS module is opt-in ; existing ROS2-only setups behave unchanged.
474
+ - `TOPICFORGE_DDS_DOMAIN_ID` — DDS domain id observed (0..232), default `0`.
475
+ - **Mock fixtures enriched**: 2 deterministic DDS participants, two-topic scenario (`/dds/well_matched` and `/dds/qos_mismatch`) exercising `detect_qos_mismatches` end-to-end.
476
+ - **`pyproject.toml` extras**: `[dds]` pulls `cyclonedds>=0.10` ; `[all]` aliases `[dds]`. `pip install topicforge` keeps the core + mock only (zero install impact on ROS2-only users).
477
+
478
+ ### Changed
479
+
480
+ - **`TopicInfo` schema soft-breaking.** Three additive optional fields (`reader_count: int | None`, `writer_count: int | None`, `qos_profile: QosProfile | None`) — all default `None`. Producer side: code constructing `TopicInfo` directly is unaffected (defaults compile). **Strict MCP clients that validated v0.1.x responses against the `TopicInfo` schema with `additionalProperties: false` will reject v0.2.0 responses unless their schema is regenerated. Standard MCP clients that read tool descriptions dynamically are unaffected.**
481
+ - **`HealthReport` schema soft-breaking**, same shape. Three additive optional fields (`dds_backend`, `dds_domain_id`, `middleware_available`) with safe defaults (`"none"`, `None`, `False`).
482
+ - **`RosAdapter` renamed to `MiddlewareAdapter`** in `adapters/base.py`. The old name remains as an alias (`RosAdapter = MiddlewareAdapter`) ; existing imports `from topicforge.adapters import RosAdapter` still type-check. The `Ros2CliAdapter.name` value moves from `"live"` to `"ros2_cli"` — internal tag, separate from the MCP-wire `mode_effective` field which keeps its `Literal["mock", "live"]` contract.
483
+ - **`Settings`** gains `dds_backend` and `dds_domain_id` fields with safe defaults (`"mock"`, `0`). Existing `Settings(...)` constructors are unaffected.
484
+ - **`Ros2CliAdapter` DDS methods raise `AdapterError`** with a clear remediation path (`pip install topicforge[dds]` + `TOPICFORGE_DDS_BACKEND=cyclone`). This is the v0.2.0 MVP limitation D6 (single-adapter-at-a-time) ; a composite adapter that delegates per-tool is a v0.2.x roadmap item.
485
+
486
+ ### Internal
487
+
488
+ - `parse_topic_info` and `parse_bag_info` parsers : `mode_effective` kwarg typed as `EffectiveMode` (`Literal["mock", "live"]`) rather than the broader `AdapterName`, cleanly separating the wire-facing mode from the implementation tag.
489
+ - New pytest marker `requires_cyclonedds` for tests that need the SDK. Auto-skips otherwise via `pytest.importorskip`.
490
+
491
+ ### Notes
492
+
493
+ - **`cyclonedds` is optional.** Default installs (`pip install topicforge`) are unchanged from v0.1.2 in dependency footprint. Only `pip install topicforge[dds]` pulls the bindings (`cyclonedds>=0.10`).
494
+ - **No code change to the 5 ROS2 tools** — `health_check`, `list_topics`, `get_topic_info`, `sample_messages`, `analyze_bag` behave identically to v0.1.2. The wire contract (`mode_effective: Literal["mock", "live"]`) is unchanged.
495
+ - **v0.2.0 MVP limitation**: single adapter at a time. Users select ROS2 introspection (default) or DDS observability via `TOPICFORGE_DDS_BACKEND=cyclone`, not both simultaneously. The unselected half raises `AdapterError` with a remediation pointer. A composite adapter delegating per-tool category is a v0.2.x roadmap item.
496
+
497
+ ## [0.1.2] - 2026-05-13
498
+
499
+ ### Fixed
500
+
501
+ - `sample_messages` now returns real publish-time timestamps in live mode for `Header`-stamped messages. The live adapter previously shelled out to `ros2 topic echo --once`, which does not emit timestamps, so `MessageSample.timestamp_ns` was always `0`. The invocation is now `ros2 topic echo --csv --once`, whose flattened CSV exposes `header.stamp.sec` and `header.stamp.nanosec` as the first two columns for any `Header`-stamped message; the new `parse_csv_echo` parser reconstructs `timestamp_ns = sec * 1_000_000_000 + nanosec` and strips those two columns out of the payload. **Headerless message types** (e.g. `std_msgs/String`, `geometry_msgs/Twist`) still return `timestamp_ns=0` — they carry no embedded timestamp. Surfacing the rmw **receive** timestamp (rather than the publish-time `header.stamp`) for arbitrary message types remains a roadmap item tied to the future `rclpy`-backed adapter.
502
+
503
+ ### Added
504
+
505
+ - **`mode_effective` on every tool response (schema, soft-breaking additive).** `TopicInfo`, `SampleResult`, and `BagAnalysis` now carry a required `mode_effective: Literal["mock", "live"]` field. A new `effective_mode` property on the `RosAdapter` protocol is the single source of truth; `Ros2CliAdapter` returns `"live"`, `MockAdapter` returns `"mock"`, services thread it through at result construction time. **Producer side**: Python code constructing these models directly must now supply `mode_effective` — models are `frozen=True, extra="forbid"` with no default. **Client side (over MCP)**: additive — an MCP client consuming JSON sees one extra key per response and is unaffected unless it strictly validates against the v0.1.1 schema with a no-extra-keys assumption.
506
+ - **DDS-MCP spec** (`docs/projet-file/mcp-02-spec.md`). Strategic draft for MCP 02 at the time: safety-first read-only DDS observability across middleware vendors (CycloneDDS OSS, RTI Connext Pro tier). Five tools, `MiddlewareAdapter` protocol, mock + cyclone + rti + auto modes. Reviewer notes appended (2026-05-13): wrong cross-reference in §11 flagged. (Reframed the next day as a TopicForge module after the mono-MCP pivot — see [0.2.0] Strategic section.)
507
+ - **DatasetForge spec** (`docs/projet-file/mcp-03-spec.md`). Vision Dataset Inspector spec, re-slotted to MCP 03 after competitive-landscape audit that surfaced zero non-ROS DDS-MCP projects and made a standalone DDS-MCP the stronger MCP 02 candidate. Reviewer notes appended (2026-05-13): contradictory §11 phrasing and two implicitly-resolved open questions flagged.
508
+
509
+ ### Changed
510
+
511
+ - **Safety-first read-only repositioning.** README and `docs/product-plan.md §1` now lead with "read-only by architecture, not by configuration" as the primary identity. Pack candidate list updated: MCP 02 reframed to a non-ROS DDS observability MCP (later folded into TopicForge itself by the 2026-05-14 multi-vendor reframe — see v0.3.0 entry); DatasetForge slides to MCP 03. Strategic context in `docs/product-plan.md §4` and §8 (DDS-complete horizon).
512
+ - **Internal API.** `Inspector.sample_messages` now returns a `SampleResult` envelope (previously a `list[MessageSample]`). The MCP-facing tool handler is reduced to a thin pass-through. No effect on the tool's wire-level response shape (handlers already wrapped the list into `SampleResult`), but flagged here for anyone importing `Inspector` directly outside this repo.
513
+
514
+ ### Internal
515
+
516
+ - Docstring fix in `parse_csv_echo`: the example output now shows post-strip payload keys as `col_0`, `col_1` (the parser re-indexes from `col_0` after dropping the two timestamp columns), matching the existing test in `tests/test_live_adapter_parse.py`.
517
+
518
+ ## [0.1.1] - 2026-05-13
519
+
520
+ ### Added
521
+
522
+ - **Opt-in anonymous usage telemetry** behind `TOPICFORGE_TELEMETRY=on` (default: off). When enabled, each MCP tool call emits a single event with six fields only: `tool_name`, `latency_ms`, `mode`, `version`, `session_id` (random UUID per process, never persisted), and `success`. No topic names, message bodies, bag paths, hostnames, or environment data ever leave the process. See the README "Telemetry" section for the full payload contract and opt-out instructions.
523
+ - `src/topicforge/telemetry/` module with `TelemetryClient`, `TelemetryEvent`, and an `instrument()` decorator that wraps tool handlers with timing + emit. When telemetry is off, `instrument()` is the identity function — zero overhead and zero possibility of a network call in the OFF code path.
524
+ - Pluggable `Transport` callable; v0.1.1 ships a structured-log transport. A future S3-backed HTTP endpoint will plug in without touching tool handlers.
525
+ - 29 telemetry tests covering: default-off behaviour, env var parsing (`on`/`1`/`true`/`yes`/`enabled` vs anything else), payload shape and key allowlist, payload privacy (user input never leaks), session id stability and per-process uniqueness, transport-exception isolation, decorator signature preservation, and end-to-end verification that the OFF code path never invokes the transport.
526
+
527
+ ### Changed
528
+
529
+ - `Settings` gained a `telemetry_enabled: bool` field.
530
+ - `build_app(...)` accepts optional `telemetry` and `telemetry_transport` parameters for test injection.
531
+ - `register_tools(...)` now takes a `TelemetryClient`.
532
+ - `.env.example` documents `TOPICFORGE_TELEMETRY`.
533
+ - README adds a `Telemetry` section and updates the Security model note to reflect opt-in telemetry availability.
534
+
535
+ ## [0.1.0] - 2026-05-12
536
+
537
+ Initial MVP release of TopicForge — ROS Topic Inspector & Bag Analyzer MCP server.
538
+
539
+ ### Added
540
+
541
+ - Five read-only MCP tools exposed over FastMCP: `health_check`, `list_topics`, `get_topic_info`, `sample_messages`, and `analyze_bag`.
542
+ - `RosAdapter` protocol in `adapters/base.py` defining the contract every backend implements.
543
+ - Mock adapter (`adapters/ros2_mock/`) with deterministic fixtures modeling a small differential mobile robot equipped with a LIDAR and an RGB camera.
544
+ - Live adapter (`adapters/ros2_live/`) built on subprocess wrappers around the `ros2` CLI, with pure module-level parsers tested independently of any ROS2 install.
545
+ - Three runtime modes selectable via `TOPICFORGE_MODE`: `mock`, `live`, and `auto`. The `auto` resolution lives in `Settings.effective_mode`; the live-to-mock fallback when the adapter cannot start lives in `services/factory.py`.
546
+ - Windows-first cross-platform support: executable resolution via `shutil.which` (handles `ros2.cmd` / `ros2.bat` shims), `subprocess.run` called with absolute paths and never `shell=True`, all filesystem paths via `pathlib.Path`.
547
+ - Pydantic v2 schemas in `models/` configured with `extra="forbid"` and `frozen=True`, returned as the structured payload of every tool.
548
+ - Pytest suite that runs entirely without a ROS2 environment, covering services, mock adapter, and live-adapter parsers.
549
+ - Build, lint, and tooling configuration: Python 3.11+, `mcp >= 1.0.0` (FastMCP), `pydantic >= 2.6`, pytest, ruff, hatchling.
550
+ - Licensed under the MIT License.
551
+
552
+ ### Notes
553
+
554
+ - The write path (publishing, commanding robots) is intentionally out of scope for the MVP.
555
+ - `analyze_bag` in live mode parses `ros2 bag info` text output; deeper anomaly detection remains mock-only for now.
556
+
557
+ [Unreleased]: https://github.com/yaniswav/TopicForge/compare/v0.3.0...HEAD
558
+ [0.3.0]: https://github.com/yaniswav/TopicForge/compare/v0.2.0...v0.3.0
559
+ [0.2.0]: https://github.com/yaniswav/TopicForge/compare/v0.1.2...v0.2.0
560
+ [0.1.2]: https://github.com/yaniswav/TopicForge/compare/v0.1.1...v0.1.2
561
+ [0.1.1]: https://github.com/yaniswav/TopicForge/compare/v0.1.0...v0.1.1
562
+ [0.1.0]: https://github.com/yaniswav/TopicForge/releases/tag/v0.1.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: topicforge
3
- Version: 0.3.0
3
+ Version: 0.4.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
@@ -43,13 +43,24 @@ Requires-Dist: pydantic>=2.6
43
43
  Provides-Extra: all
44
44
  Requires-Dist: cyclonedds>=0.10; extra == 'all'
45
45
  Requires-Dist: fastdds<3,>=2.6.1; extra == 'all'
46
+ Provides-Extra: bags
47
+ Requires-Dist: rosbags>=0.9; extra == 'bags'
46
48
  Provides-Extra: dds
47
49
  Requires-Dist: cyclonedds>=0.10; extra == 'dds'
48
50
  Requires-Dist: fastdds<3,>=2.6.1; extra == 'dds'
51
+ Provides-Extra: dds-all-oss
52
+ Requires-Dist: cyclonedds>=0.10; extra == 'dds-all-oss'
53
+ Requires-Dist: dust-dds-python>=0.1; extra == 'dds-all-oss'
54
+ Requires-Dist: fastdds<3,>=2.6.1; extra == 'dds-all-oss'
55
+ Requires-Dist: pyopendds>=0.1; extra == 'dds-all-oss'
49
56
  Provides-Extra: dds-cyclone
50
57
  Requires-Dist: cyclonedds>=0.10; extra == 'dds-cyclone'
58
+ Provides-Extra: dds-dust
59
+ Requires-Dist: dust-dds-python>=0.1; extra == 'dds-dust'
51
60
  Provides-Extra: dds-fast
52
61
  Requires-Dist: fastdds<3,>=2.6.1; extra == 'dds-fast'
62
+ Provides-Extra: dds-opendds
63
+ Requires-Dist: pyopendds>=0.1; extra == 'dds-opendds'
53
64
  Provides-Extra: dev
54
65
  Requires-Dist: pytest>=8.0; extra == 'dev'
55
66
  Requires-Dist: ruff>=0.4; extra == 'dev'
@@ -200,7 +211,7 @@ Beyond ROS2 graph introspection, TopicForge observes the raw DDS bus directly vi
200
211
 
201
212
  Useful for non-ROS DDS stacks (defense, aerospace, automotive AUTOSAR Adaptive, industrial integration) and for diagnosing why a ROS2 subscriber isn't receiving when the graph says it should. Same safety-first contract : read-only by **architecture** — the `MiddlewareAdapter` protocol does not expose a write method on any backend.
202
213
 
203
- Install one or both OSS backends :
214
+ Install one or both OSS backends — the v0.4.0 auto-detect framework picks whichever you actually installed:
204
215
 
205
216
  ```bash
206
217
  # Single vendor — install only what you need
@@ -209,16 +220,25 @@ pip install topicforge[dds-fast] # eProsima Fast DDS only
209
220
  pip install topicforge[dds] # both OSS backends (union)
210
221
  ```
211
222
 
212
- Then select a backend :
223
+ Then select a backend (or let auto-detect pick):
213
224
 
214
225
  ```bash
215
226
  TOPICFORGE_MODE=live TOPICFORGE_DDS_BACKEND=cyclone python -m topicforge
216
227
  # or:
217
228
  TOPICFORGE_MODE=live TOPICFORGE_DDS_BACKEND=fast python -m topicforge
218
- # or, auto-select Fast > Cyclone > Mock:
229
+ # or, auto-select across the 8-vendor priority chain:
230
+ # RTI > OpenSplice > CoreDX > InterCOM (Pro tier, if installed)
231
+ # > OpenDDS > Fast > Cyclone > Dust > Mock (OSS fallback)
219
232
  TOPICFORGE_MODE=live TOPICFORGE_DDS_BACKEND=auto python -m topicforge
220
233
  ```
221
234
 
235
+ **OSS vs Pro tier (v0.4.0+).** The OSS install above covers the community
236
+ DDS adapters (Cyclone, Fast — plus OpenDDS / Dust stubs awaiting upstream
237
+ Python bindings). The commercial DDS adapters (RTI Connext, OpenSplice,
238
+ CoreDX, InterCOM) ship under the optional `topicforge-pro` package with
239
+ BYO vendor license. See [`docs/pro.md`](docs/pro.md) for the early-access
240
+ slot and pricing terms ; nothing is collected today.
241
+
222
242
  Three new MCP tools (in addition to the five ROS2 tools above) :
223
243
 
224
244
  | Tool | Purpose |
@@ -227,7 +247,7 @@ Three new MCP tools (in addition to the five ROS2 tools above) :
227
247
  | `detect_qos_mismatches` | Reader/writer QoS incompatibilities preventing communication on a topic |
228
248
  | `peek_dds_samples` | Recent samples on a raw DDS topic (distinct from `sample_messages` on ROS2 graph)|
229
249
 
230
- **v0.3.0 limitation — single adapter at a time.** TopicForge selects one adapter per server run. With `TOPICFORGE_DDS_BACKEND=cyclone` (or `fast`), the 3 DDS tools work and the 5 ROS2 tools raise a clear `AdapterError` ; vice versa with the default `TOPICFORGE_DDS_BACKEND=mock`. The mock backend exposes all 8 tools against deterministic fixtures (incl. a `vendor="fast"` participant) for local development. A composite adapter delegating per-tool category is a v0.3.x roadmap item.
250
+ **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 3 DDS tools (+ `participant_events` from Phase 1) 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 9 tools against deterministic fixtures for local development.
231
251
 
232
252
  **v0.3.0 limitation — `peek_dds_samples` scope.** Full-fidelity on the 4 builtin DCPS topics (`DCPSParticipant`, `DCPSSubscription`, `DCPSPublication`) ; arbitrary user topics raise an `AdapterError` pointing at the v0.3.x XTypes/IDL roadmap. The other two DDS tools (`list_participants`, `detect_qos_mismatches`) work end-to-end on any user-topic deployment.
233
253