Unseals the Sparkplug branch of the MQTT address-picker browser that Task 10
deliberately sealed shut, and adds the first (and only) sanctioned publish on a
browse session.
- MqttBrowseSession serves Group -> EdgeNode -> [Device] -> Metric in Sparkplug
mode, decoded from observed NBIRTH/DBIRTH certificates (the only Sparkplug
messages that name their metrics). Node-level metrics hang directly under
their edge node -- no synthesised device folder, because the authored address
tuple genuinely has deviceId = null for them. Metric node ids use a '::'
separator: a metric name may contain '/', so slash-joining would make a
node-level metric indistinguishable from a device folder.
- AttributesAsync is purely birth-derived in Sparkplug mode. The plain path's
UTF-8 inference / payload-snippet machinery does not run and no payload bytes
are retained: a Sparkplug body is protobuf and would be reported as
"(N bytes, binary)" for every metric in the plant. A datatype ToDriverDataType
cannot map is reported as the Sparkplug type name, never coerced.
- RequestRebirthAsync(scope) is the one publishing member, reached only by an
explicit operator action. It routes through the counted PublishAsync
chokepoint via a private RebirthTransport adapter, so PublishCountForTest
still proves that OpenAsync/RootAsync/ExpandAsync/AttributesAsync/Observe/
DisposeAsync publish nothing -- now in both modes. A device or metric scope
resolves up to its owning edge node (NCMD is node-addressed); group scope
enumerates observed edge nodes and is refused whole past
MaxGroupRebirthNodes = 32.
- BrowserSessionService.RequestRebirthAsync gates on the same DriverOperator
policy that gates the picker, evaluated server-side where the action happens
rather than only at render time, fails closed, and Info-logs the scope and the
published count. Exposed through the new IRebirthCapableBrowseSession contract
so "does this session write?" stays a type question.
- ToBrowseOptions' Last-Will landmine re-verified: MqttDriverOptions still
carries nothing will-shaped (an NDEATH is a broker-published will and would be
invisible to PublishCountForTest), now pinned by a reflection guard.
Falsifiability: injecting a publish into RootAsync reddens the Sparkplug and
plain passivity tests; double-publishing per target reddens all four
one-NCMD-per-node assertions. 572/572 MQTT tests, 24/24 AdminUI browsing tests.
Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
Task 9: DiscoverTreeAsync UntilStable settle loop (FOCAS) — re-capture on a 1s interval until
the node set is non-empty and stable across two passes, bounded by open-timeout; on
timeout with a prior non-empty capture, return it (best-effort) instead of failing.
Task 11: BrowserSessionService resolves bespoke-first, falls back to IUniversalDriverBrowser
when no bespoke browser matches and CanBrowse is true; new CanBrowse on the service.
16 unit tests green (4 settle + 12 service).