Iter 21. Lands ADR-122 §2.2 topic shape + class-gated routing as a pure
function. No broker dep yet — that lands in iter 22 with tokio + rumqttc
behind an `mqtt` feature. This iter is the routing policy, separated for
testability.
Added (gated on `feature = "std"`):
- src/mqtt_topics.rs:
* TopicMessage { topic: String, payload: String }
* TopicMessage::ruview_topic(node, entity) builds the canonical
`ruview/<node>/bfld/<entity>/state` shape
* render_events(&BfldEvent) -> Vec<TopicMessage>:
class < Anonymous (0/1): returns empty (raw/derived are local only)
class >= Anonymous (2/3): emits presence + motion + person_count +
confidence, plus zone_activity if zone_id set
class == Anonymous (2) ONLY: also emits identity_risk
class == Restricted (3): identity_risk is suppressed even with score
- pub use render_events, TopicMessage from lib.rs
Payload encoding:
- presence: "true" | "false"
- motion: "{:.6}" — fixed-precision decimal in [0.0, 1.0]
- person_count: bare integer string
- confidence: "{:.6}"
- zone_activity: JSON-string with quotes — "\"living_room\""
- identity_risk: "{:.6}"
tests/mqtt_topic_routing.rs (10 named tests, all green):
topic_format_is_ruview_node_bfld_entity_state
anonymous_class_publishes_six_topics_with_zone
(6 = presence/motion/count/conf/zone/identity_risk)
anonymous_class_without_zone_omits_zone_activity_topic (5 topics)
restricted_class_omits_identity_risk_topic (class 3 → 5 topics, no risk)
raw_and_derived_classes_publish_nothing
*** structural enforcement of "raw stays local" at the topic layer ***
presence_payload_is_lowercase_json_bool
motion_payload_is_fixed_precision_decimal
person_count_payload_is_bare_integer
zone_payload_is_json_string_with_quotes
identity_risk_payload_is_fixed_precision_decimal
ACs progressed:
- ADR-122 §2.2 topic shape now matches the documented format byte-for-byte.
- ADR-122 AC4 — per-class topic gating: classes 2 / 3 publish disjoint
sets, with identity_risk uniquely guarded.
- ADR-118 invariant I1 reaching the public surface — Raw frames produce
zero topic messages, so even a buggy publisher loop cannot leak them.
Test config:
- cargo test --no-default-features → 72 passed (mqtt_topics cfg-out)
- cargo test → 162 passed (152 + 10)
Out of scope (next iter target):
- tokio + rumqttc behind a new `mqtt` feature gate
- BfldPipelineHandle: Arc<Mutex<BfldPipeline>> + a tokio task that pumps
inbound SensingInputs, runs render_events on each emitted BfldEvent,
and calls client.publish() for each TopicMessage
- mosquitto integration test pattern (cf. feedback_mqtt_integration_test_patterns
memory: per-test client_id, pump until SubAck, wait for publisher discovery)
Co-Authored-By: claude-flow <ruv@ruv.net>