Iter 22. Abstracts the MQTT publish boundary without pulling in tokio or
rumqttc yet. The trait is sync (callers can hold &mut self without an
async runtime); the production rumqttc-backed impl in iter 23 will drive
a tokio task internally and present the same sync surface here.
Added (in src/mqtt_topics.rs, gated on `feature = "std"`):
- Publish trait with associated Error type
- CapturePublisher (Vec-backed; default-constructible) for unit tests
- publish_event<P: Publish>(publisher, event) -> Result<usize, P::Error>
Iterates render_events(event) and forwards each TopicMessage to
publisher.publish(). Returns the count actually published, or the
publisher's error short-circuited on first failure.
- pub use Publish, CapturePublisher, publish_event from lib.rs
tests/mqtt_publish_loop.rs (7 named tests, all green):
capture_publisher_records_every_message
publish_returns_zero_for_raw_and_derived_events
(parameterized — class 0 and class 1 both produce zero publishes,
reinforcing the invariant I1 surface enforcement from iter 21)
published_topics_match_render_events_ordering
(stable per-event topic sequence for MQTT consumers)
restricted_class_publishes_no_identity_risk_topic
anonymous_without_zone_publishes_five_messages (5 = no zone_activity)
publisher_error_short_circuits_publish_event
(FailingPublisher fails on 3rd publish; publish_event surfaces the
error AND leaves the first two messages durably published)
capture_publisher_error_type_is_infallible
(compile-time witness that CapturePublisher cannot panic the loop)
ACs progressed:
- ADR-122 §2.2 publisher boundary — the broker-facing surface is now a
named trait operators can mock, swap, or wrap with retries.
- ADR-122 AC4 — publish_event respects the iter-21 class gating; Raw /
Derived events produce zero broker traffic by definition.
- ADR-118 invariant I1 — even if the broker connection somehow regressed,
the trait-level publish_event cannot exfiltrate a Raw frame because
render_events returns empty first.
Test config:
- cargo test --no-default-features → 72 passed (mqtt_publish_loop cfg-out)
- cargo test → 169 passed (162 + 7)
Out of scope (next iter target):
- New `mqtt` feature gate; tokio + rumqttc deps under it
- RumqttPublisher: impl Publish that holds an MqttClient + a small tokio
block_on or oneshot send to bridge sync trait to async client
- Optional: BfldPipelineHandle that owns Arc<Mutex<BfldPipeline>> + a
spawn-and-forget tokio task pumping inbound (inputs, embedding) →
process → publish_event(&rumqtt_pub, &event)
- mosquitto integration test following the patterns from
feedback_mqtt_integration_test_patterns memory note
Co-Authored-By: claude-flow <ruv@ruv.net>