fix(opcuaserver): UNS reference nodes repair on backing-tag historian/shape edits
Adversarial review of 9f68f272 (classifier: non-alarm raw-tag deltas became
surgical) found stale UNS inheritance after a surgical raw edit. Editing a raw
tag's IsHistorized / HistorianTagname / IsArray / ArrayLength left every
referencing UNS node STALE: none of those fields were on UnsReferenceVariable,
so no coincident UnsReferenceDelta fired; the applier updated the raw node in
place; and EnsureVariable early-returns on an existing node. Consequences:
HistoryRead through the UNS NodeId served a different series than through the
raw NodeId (breaking the documented dual-registration invariant), a
de-historized tag kept serving history via UNS, and a scalar->array flip left
the UNS mirror scalar AND writable (bypassing the array read-only write gate).
UnsReferenceVariable now carries Backing{IsHistorized,HistorianTagname,IsArray,
ArrayLength}, copied from the backing tag's TagConfigIntent at compose time, so
record equality makes the reference Changed in the SAME plan. Both deltas are
non-rebuild kinds, so the existing machinery repairs the mirror surgically:
remove-previous in Apply, recreate in MaterialiseUnsReferences. That pass now
reads the four values from the RECORD (the single truth the planner diffed)
instead of a rawByPath back-lookup, preserving historian resolution exactly
(blank override => BackingRawPath, which is the backing tag's NodeId).
The artifact-decode seam needs no change: DeploymentArtifact.BuildRawAndUnsSubtrees
drives AddressSpaceComposer.Compose verbatim, which is what guarantees byte-parity.
The classifier needs no change either (a UnsReferenceDelta never forces Rebuild);
its comment now records why.
Also from the same review: a three-place-contract comment on RawTagPlan's
hand-written Equals naming its two downstream consumers, plus classifier pins for
the .Any(!eligible) quantifier, an ArrayLength-only delta, and an
alarm-content-only delta.
This commit is contained in:
@@ -74,6 +74,19 @@ public static class AddressSpaceChangeClassifier
|
||||
// • ChangedUnsReferenceVariables ⇒ surgical remove-previous + recreate-current, applier-owned (a UNS
|
||||
// reference variable is a mirror node binding no driver, so it carries no notifier wiring of its
|
||||
// own — the alarm fan-out it participates in is re-derived from the raw tag's referencing set).
|
||||
// No rebuild is forced for a UNS-reference delta in EITHER direction, and that is load-bearing for
|
||||
// the repair path below.
|
||||
// UNS-REFERENCE STALENESS, and why nothing here gates it (2026-08-15 review). Making non-alarm raw-tag
|
||||
// deltas surgical meant a raw-side IsHistorized / HistorianTagname / IsArray / ArrayLength edit updated
|
||||
// the raw node in place while every referencing UNS MIRROR node kept the old historian tagname + shape
|
||||
// (EnsureVariable early-returns on an existing node) — wrong history series through the UNS NodeId, and
|
||||
// a scalar→array flip left the mirror scalar AND writable. The fix is in the RECORD, not the classifier:
|
||||
// UnsReferenceVariable now carries the backing tag's Backing{IsHistorized,HistorianTagname,IsArray,
|
||||
// ArrayLength}, so the same edit makes the reference record unequal and the planner emits a COINCIDENT
|
||||
// UnsReferenceDelta alongside the RawTagDelta. Both are non-rebuild kinds, so the plan stays
|
||||
// AttributeOnly and the applier repairs the mirror surgically (remove-previous + materialise-recreate).
|
||||
// Adding a new inherited field to UnsReferenceVariable therefore needs no change here — but OMITTING one
|
||||
// silently reopens the staleness.
|
||||
// Anything a future surgical raw-tag path lets through that DOES touch alarm notifiers must still
|
||||
// re-wire/unwire them (WireAlarmNotifiers is a reconcile — pass the tag's COMPLETE current referencing
|
||||
// set — plus an explicit unwire on removal), else a de-referenced equipment keeps receiving the events.
|
||||
|
||||
Reference in New Issue
Block a user