feat: always carry the canonical componentPath in the update extra - #7
Merged
Conversation
Every published SouthboundSignalUpdate now carries the signal's full, untruncated MTConnect component path as `componentPath` on the update-level extra map — unconditionally, for a truncated derived channel, an untruncated one and an explicitly configured signal alike, so an MTConnect-aware consumer reads one field with no branch. * Placement is the update level, not the sample: the path is per-signal-static and one update is one signal's readings, so a batched window carries exactly one of them. The library's SouthboundSignalUpdate reserves only `signal` and `samples` there and round-trips every other body key through the protobuf `extra` map, so this is a first-class round-trip; `componentPath` collides with none of the seven reserved sample keys. * One source of truth for the value: ProbeModel::component_path_of, which address_of (what sb/signals and sb/browse serve) now also reads. The update and the control plane cannot drift apart. * Three values, all present: the slash-joined path; "" for a device-level data item; null for a signal no device model describes — the permanent-BAD case, where sb/signals reports address.componentPath: null and the update says the same. The key is never omitted. * No core change. The SignalUpdate builder has no update-level extra setter, so the supervisor uses the facade's own two-step form: build_body applies the whole SOUTHBOUND §2.1 contract, stamp_component_path adds the one additive key, publish_body_via mints the topic and stamps identity. Both structural checks publish() makes are re-made rather than dropped. Recorded as D-MtconnectAdapter-L13, with the two rejected alternatives: truncation-conditional stamping (conditional reader code, inconsistent presence) and promoting the path to a first-class address block (a four-language wire-contract change for one protocol's shape).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Every published
SouthboundSignalUpdatenow carries the signal's full, untruncated MTConnect component path ascomponentPathon the update-level extra map — unconditionally, for a truncated derived channel, an untruncated one and an explicitly configured signal alike. One consistent place, no conditional branch in reader code.Placement — verified against the core, not assumed
Checked against
proto/edgecommons/v1/telemetry.protoand the Rust codec at the pinned rev (a14a328):SouthboundSignalUpdatedeclaresmap<string, EcValue> extra = 100.to_telemetrycollects every body key that is notsignal/samplesinto that map (copy_extra(obj, &["signal", "samples"]));from_telemetryputs them back. So the update-level extra genuinely round-trips — it is what already carries thedeviceblock today.componentPathcollides with nothing: the seven reserved sample keys arevalue,quality,qualityRaw,sourceTs,serverTs,sourceTsMs,serverTsMs; the update level reserves onlysignalandsamples. No fallback tosourcePathwas needed.EcValuehas aNullValuearm, so a JSONnullsurvives the codec — which is what makes unconditional presence possible in every case.tests/component_path.rsasserts this on the decoded body after a realMessage::to_vec()→Message::from_slice()round-trip, not on the JSON.The probe-absent edge case
An explicit
signals[]entry whosedataItemIdis not in the probe model is the permanent-BAD case (MTC_NO_SUCH_DATAITEM).sb/signalsdoes not serve nothing for it —unlearned_addressservesaddress.componentPath: null. So the update stampsnulland the key is still present. There is no exception: the field is on every update, always.Three values, all of them present:
"Axes[Axes]/Linear[X]"""Lavailon the live demo Okuma).nullnullsb/signalsreports.One source of truth
ProbeModel::component_path_ofis the only formatter.address_of(whatsb/signalsandsb/browseserve) now reads it too, so the update and the control plane cannot drift apart.Reading.component_pathcarries it through the seam; the derived channel may be shortened to the UNS topic budget (L12), this never is.No core change
The
SignalUpdatebuilder has no update-levelextrasetter, sosupervisor.rsuses the facade's own two-step form rather than hand-building anything:DataFacade::build_body(the whole SOUTHBOUND §2.1 contract — quality defaulting, thequalityRawmarker, theserverTsfill, the samples wrapper) ▸app::stamp_component_path(one additive key) ▸DataFacade::publish_body_via(mintsdata/{channel}from the sameeffective_signal_path, stamps identity, honours the per-call channel override). Both structural checkspublish()makes are re-made here rather than dropped.Live evidence
demo.mtconnect.orgwas up; the real probe parsed through this adapter's ownProbeModel:The README's captured Okuma envelope now shows the field with that verified value.
Tests & gates
cargo test— 390 passing, 0 failing (308 unit + 82 integration), of which 12 new: 7 unit inapp.rs(unconditional presence, exact key, no reserved-name collision,"",null, truncated/untruncated/explicit parity, idempotent stamping), 4 seam tests indevice.rs(value equals whatsb/signalsserves, device-level"", derived-signal untruncated path, probe-absent →None), plus 5 in the newtests/component_path.rs(protobuf round-trip at the update level, deep path,""/null, batched window carries it once, immediate/flush paths keep it) and extended assertions in the deep-pathpoll_acquisitione2e and the liveagent_integrationcase.cargo clippy --all-targets -- -D warnings— clean.cargo llvm-cov --fail-under-lines 90— 96.68% lines (97.16% regions), gate 90.Docs
docs/reference/messaging-interface.md— newcomponentPathsection with the value table, the "always present, no exception" statement, and the update-vs-channel distinction; the body example is now MTConnect-shaped.docs/explanation.md— why presence is unconditional rather than truncation-conditional.DESIGN.md— D-MtconnectAdapter-L13 records the user decision and the two rejected alternatives: (a) truncation-conditional stamping (conditional reader code, presence becomes a function of the topic budget) and (b) promoting the path to a first-class address block (a four-language wire-contract change for one protocol's shape; reconsider only if a second deep-path protocol appears). L12's "nothing is lost" bullet updated.