Commit Graph

62 Commits

Author SHA1 Message Date
Joseph Doherty 5534698d70 feat(v3-batch4-wp2): node manager dual-namespace + realm-aware sink surface
Register both v3 namespaces (Raw first, UNS second) on OtOpcUaNodeManager and
thread an AddressSpaceRealm discriminator through every node-naming sink method
so a bare node id is resolved to the correct namespace by realm — never parsed
out of the id string.

- IOpcUaAddressSpaceSink / ISurgicalAddressSpaceSink: every node-naming method
  gains `AddressSpaceRealm realm = AddressSpaceRealm.Uns` (transitional default so
  un-migrated WP3 call sites still compile; WP3/Wave B makes them explicit and
  removes the default). Null + SdkAddressSpaceSink impls updated; DeferredAddress-
  SpaceSink forwards realm through every method (the forwarding trap).
- DeferredSinkForwardingReflectionTests: existing exhaustive-forwarding guard
  auto-covers the new signatures; added an explicit guard that every node-naming
  sink method carries an AddressSpaceRealm parameter (RebuildAddressSpace exempt).
- OtOpcUaNodeManager: register RawNamespaceUri + UnsNamespaceUri; realm->namespace
  index via NamespaceIndexForRealm; all node maps (_variables/_folders/
  _alarmConditions/_nativeAlarmNodeIds/_historizedTagnames/_eventNotifierSources)
  re-keyed by the full ns-qualified NodeId string so Raw and UNS nodes sharing a
  bare id stay distinct; _notifierFolders already NodeId-keyed. A historized UNS
  reference node registers the SAME historian tagname as its backing raw node
  (both NodeIds -> one tagname). Inbound-write hook routes the full ns-qualified
  NodeId to the write gateway (realm-aware) and reverts by bare id + realm.
  HistoryRead seams resolve via NodeId directly. DefaultNamespaceUri kept as a
  transitional alias to UnsNamespaceUri for the SubscriptionSurvivalTests.
- Test doubles across Commons.Tests / OpcUaServer.Tests / Runtime.Tests updated to
  the new interface signatures; SdkAddressSpaceSinkTests asserts the UNS namespace
  index for default-realm nodes.

Build: dotnet build ZB.MOM.WW.OtOpcUa.slnx = 0 errors.
Tests: Commons.Tests 310/310; OpcUaServer.Tests 335 passed / 4 pre-existing skips.

Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
2026-07-16 09:33:45 -04:00
Joseph Doherty 04fa2f5cb2 Merge R2-07 Surgical pure-adds (arch-review round 2) [PR #436]
v2-ci / build (push) Successful in 3m19s
v2-ci / unit-tests (push) Failing after 8m13s
Finding 03/P1: surgical in-place address-space adds/removals skip full rebuilds
(AddressSpaceChangeClassifier default-closed to Rebuild). 3 new
ISurgicalAddressSpaceSink remove members, guard-first + forwarded through
DeferredAddressSpaceSink (F10b inertness net green). T5/T6/T12/T14 over-the-wire +
docker-dev gates deferred. Auto-merged OtOpcUaNodeManager.cs with R2-08; verified
OpcUaServer.Tests 341/341 + forwarding guard 3/3. Build clean.
2026-07-13 12:27:59 -04:00
Joseph Doherty 3cebfae417 feat(opcuaserver): process-wide HistoryRead batch limiter — flood degrades to BadTooManyOperations, not pool exhaustion (03/S3) 2026-07-13 12:18:35 -04:00
Joseph Doherty 888cf86655 feat(opcuaserver): per-request HistoryRead deadline — hung backends surface BadTimeout (03/S3) 2026-07-13 12:13:38 -04:00
Joseph Doherty 4504ed930c feat(opcua): surgical RemoveEquipmentSubtree with notifier demotion (R2-07 T10) 2026-07-13 12:12:09 -04:00
Joseph Doherty 9653493905 feat(opcua): surgical RemoveAlarmConditionNode (R2-07 T9) 2026-07-13 12:09:55 -04:00
Joseph Doherty 8c365da754 feat(opcuaserver): bounded per-node parallel HistoryRead within a batch (03/S3) 2026-07-13 12:09:53 -04:00
Joseph Doherty 14003ab9b8 feat(opcua): surgical RemoveVariableNode with NodeDeleted model-change (R2-07 T8) 2026-07-13 12:09:14 -04:00
Joseph Doherty 438004e371 feat(commons): add surgical remove members to ISurgicalAddressSpaceSink + deferred forwarding (guard-verified) (R2-07 T7) 2026-07-13 12:07:01 -04:00
Joseph Doherty cef7d9b281 refactor(opcuaserver): async-ify ServeRawPaged + HistoryReadEvents internals (03/S3) 2026-07-13 12:07:00 -04:00
Joseph Doherty 1e83fe7085 refactor(opcuaserver): async-ify ServeNode + Processed/AtTime HistoryRead internals, thread cancellation (03/S3) 2026-07-13 12:04:54 -04:00
Joseph Doherty 304442dba4 feat(runtime): ServerHistorian HistoryRead parallelism/limiter/deadline knobs + node-manager wiring (03/S3) 2026-07-13 12:03:27 -04:00
Joseph Doherty a86d9f65bf feat(opcua): make MaterialiseAlarmCondition idempotent for same-kind re-applies (R2-07 T4a) 2026-07-13 11:50:14 -04:00
Joseph Doherty 9cad9ed0fc docs(xmldoc): fill missing XML docs + strip tracking-ID comments across src
v2-ci / build (push) Failing after 41s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
Adds <summary>/<param>/<returns>/<inheritdoc> where missing and removes
project bookkeeping IDs (task/tracking refs) from shipped code comments,
so the docs read cleanly and CommentChecker is quiet except for known
false positives (PLC/protocol terms, event/IEqualityComparer inheritdoc).
Doc/comment-only; no logic changed; solution builds clean.
2026-07-07 12:38:39 -04:00
Joseph Doherty f8406d348c fix(otopcua): report NodeAdded model-change outside the node Lock 2026-06-26 07:06:43 -04:00
Joseph Doherty 33b0e639a5 feat(otopcua): GeneralModelChangeEvent(NodeAdded) for runtime node adds 2026-06-26 06:55:52 -04:00
Joseph Doherty 23b42b424d fix(code-review): resolve OpcUaServer-001 — UNS Area/Line rename refreshes folder DisplayName
A rename-only deploy produced an IsEmpty plan that short-circuited before MaterialiseHierarchy,
leaving the OPC UA folder DisplayName stale. AddressSpacePlanner now diffs UnsAreas/UnsLines by
stable id into a RenamedFolders set (counted in IsEmpty); the applier refreshes the folder in
place via a new UpdateFolderDisplayName on ISurgicalAddressSpaceSink (forwarded through
DeferredAddressSpaceSink so it is NOT inert on driver hosts; falls back to rebuild when the sink
is non-surgical). DeploymentArtifact byte-parity untouched (rename rides the existing Name
round-trip). No EF migration, no serialized wire/proto contract change. +13 OpcUaServer tests, Runtime rebuild test.
2026-06-20 23:10:24 -04:00
Joseph Doherty 94eec70fb0 fix(code-review): resolve Batch 3 wave A (OpcUaServer history/guard, ControlPlane topology gate)
- OpcUaServer-002: HistoryRead-Events NumValuesPerNode==0 now maps to unbounded (int.MaxValue) instead of the backend default-cap sentinel; no Core.Abstractions contract change (+EventMaxEvents helper tests)
- OpcUaServer-004: EnsureAddressSpaceCreated guard on public mutators -> clear InvalidOperationException instead of bare NRE if called pre-start (+tests)
- OpcUaServer-003: Deferred (endUtc inclusive/exclusive needs live Wonderware boundary confirmation)
- Configuration-013: wire DraftValidator.ValidateClusterTopology into AdminOperationsActor deploy gate (read-only, no migration) (+2 tests)
2026-06-20 22:53:29 -04:00
Joseph Doherty 40749d3f67 review(OpcUaServer): fix silent auto-unshelve failure (empty User -> 'system')
Cross-module fix from the review sweep. -007 (Medium): OnTimedUnshelve built its AlarmCommand
with User=string.Empty, so Part9StateMachine.ApplyUnshelve rejected it (ArgumentException,
swallowed) and a TimedShelve never auto-expired. Pass the canonical 'system' user; the
AlarmAck-gate bypass is preserved. Repurposed the test that had encoded the bug.
2026-06-19 12:29:40 -04:00
Joseph Doherty bac6613dd2 review(OpcUaServer): record findings + fix stale node-manager/host docs
First review of the v2 OPC UA core at HEAD 7286d320. 6 findings (2 Medium, 4 Low).
OpcUaServer-006 fixed (stale NodeManager/ApplicationHost XML docs). 001-004 deferred
(cross-module: Runtime publish-actor / Core.Abstractions history contract / Wonderware
boundary semantics, or latent-only). 005 re-triaged Won't-Fix (coverage already exists).
High-scrutiny paths (Lock discipline, OnWriteValue fire-and-forget, WriteOperate/AlarmAck
gates, HistoryRead AccessLevel bits) verified correct.
2026-06-19 10:37:00 -04:00
Joseph Doherty 7a8ae9600b refactor(opcua): FB-7 review nits — model-event SourceNode=Server, dataType guard, shape tests
Code-review follow-ups on the FB-7 surgical shape-write commit:
- GeneralModelChangeEvent now sets SourceNode=Server + SourceName (Part 3 §8.7.4)
  so clients filtering events by SourceNode match it (report still uses source:null).
- UpdateTagAttributes adds an explicit dataType null/empty guard (widened surface).
- Tighten the ArrayLengthDiffers doc comment.
- Add array→scalar transition test + null-arrayLength zero-default test (coverage
  symmetry). 275/275 OpcUaServer.Tests green.
2026-06-19 03:30:29 -04:00
Joseph Doherty fb094fa566 feat(opcua): FB-7 surgical DataType/array-shape in-place tag writes
Widen the F10b surgical address-space path so a changed equipment tag whose
only differences are DataType / IsArray / ArrayLength (on top of the existing
Writable / Historizing) is applied IN PLACE on the live node instead of forcing
a full RebuildAddressSpace that drops every client's subscriptions server-wide.

ISurgicalAddressSpaceSink.UpdateTagAttributes gains (dataType, isArray,
arrayLength); the DeferredAddressSpaceSink wrapper forwards all six args (the
prod-inertness seam). OtOpcUaNodeManager swaps DataType + ValueRank +
ArrayDimensions in place, and on a real shape change (a) resets the node to
BadWaitingForInitialData so no stale wrong-typed value is exposed (closes the
prior brief-value-type-mismatch objection) and (b) raises a Part 3
GeneralModelChangeEvent (verb=DataTypeChanged) so model-aware clients re-read
the definition. A Writable/Historizing-only change leaves the shape untouched
(no reset, no model event) — original behaviour preserved byte-for-byte.

AddressSpaceApplier.TagDeltaIsSurgicalEligible adds the three shape fields to
its whitelist; FullName/Name/DriverInstanceId/alarm differences still rebuild.

Tests: new NodeManagerSurgicalShapeUpdateTests boots a real server to prove the
in-place swap + value reset + the no-reset backward-compat path + the model-event
builder; AddressSpaceApplierTests invert the two former DataType/IsArray-rebuild
cases to surgical and assert the shape args land; DeferredAddressSpaceSinkTests
assert the shape args forward. 273/273 OpcUaServer.Tests green; full solution builds.
2026-06-19 03:21:03 -04:00
Joseph Doherty 36eb14e88d feat(opcua): emit Bad blip + AuditWriteUpdateEvent + sync fail-fast on failed device write 2026-06-19 02:14:58 -04:00
Joseph Doherty 40e8a23e7c refactor(opcuaserver): rename Phase7* address-space pipeline to AddressSpace*
v2-ci / build (push) Failing after 37s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
The OPC UA address-space build pipeline was named after a v2-roadmap
milestone number rather than its domain. Rename the family to describe
what it does (build/diff/apply the OPC UA address space):

  Phase7Composer          -> AddressSpaceComposer
  Phase7CompositionResult -> AddressSpaceComposition
  Phase7Planner           -> AddressSpacePlanner
  Phase7Plan              -> AddressSpacePlan
  Phase7Applier           -> AddressSpaceApplier
  Phase7ApplyOutcome      -> AddressSpaceApplyOutcome

The 9 Phase7*Tests suites follow suit; Phase7ScriptingEntitiesTests ->
ScriptingEntitiesTests (it tests the scripting migration, not the
pipeline). Log-message prefixes move to the new class names.

Pure mechanical rename, no behavioral change. EF migration classes/IDs
(AddPhase7ScriptingTables, ExtendComputeGenerationDiffWithPhase7) are
immutable and left untouched, as are historical design docs.

Build clean; OpcUaServer 261/261, Runtime 272/272, ScriptingEntities
12/12 green.
2026-06-18 19:16:28 -04:00
Joseph Doherty 3fc258bd42 feat(opcua): add ISurgicalAddressSpaceSink + node-manager in-place tag-attribute update (F10b) 2026-06-18 13:32:53 -04:00
Joseph Doherty 0f929ae668 fix(historian): defensive over-fetch cap + Validate gating comment (review) 2026-06-17 20:22:07 -04:00
Joseph Doherty 2e6c6d3ab6 feat(historian): page within oversized tie clusters (#400) instead of loud-failing 2026-06-17 20:11:09 -04:00
Joseph Doherty a792820283 feat(opcua): EnsureVariable array params (ValueRank=OneDimension + ArrayDimensions) 2026-06-16 21:16:07 -04:00
Joseph Doherty a6d9de091b feat(alarms): native condition Acknowledge routes to NativeAlarmAckRouter with principal [H6c] 2026-06-15 14:33:58 -04:00
Joseph Doherty be6858baa1 fix(alarms): OnEnableDisable native-check via lock-guarded IsNativeAlarmNode + unstale AlarmCommand doc (code-review) 2026-06-15 14:30:17 -04:00
Joseph Doherty 328bd1b9ee feat(alarms): wire OnEnableDisable over OPC UA (AlarmAck-gated; native→BadNotSupported) [H4] 2026-06-15 14:24:19 -04:00
Joseph Doherty 418663b359 feat(alarms): thread isNative through MaterialiseAlarmCondition; node manager tracks native conditions [H6a] 2026-06-15 14:13:30 -04:00
Joseph Doherty bea0b482d4 fix(historian): address code review on Raw HistoryRead paging
C1 (critical): a boundary tie cluster larger than NumValuesPerNode could
silently truncate a resumed read to GoodNoData, permanently dropping the
un-emitted ties — the (timestamp, skip) cursor cannot advance past a single
timestamp the fixed-(start,end,cap) backend keeps re-returning. Now detected
and failed LOUDLY per node with BadHistoryOperationUnsupported + a log naming
the tag/timestamp/cap; documented in Historian.md with the larger-cap remedy.
Regression test Raw_tie_cluster_larger_than_page_fails_loudly_not_silently.

I3: build HistoryData before Save() so a projection failure can never orphan a
stored continuation cursor.

N1 (YAGNI): drop the never-produced HistoryReadKind enum + Processed-only
Aggregate/IntervalTicks fields from HistoryContinuationState — only Raw pages.

N3: ComputeResumeCursor guards its documented non-empty precondition.

I1: document InMemoryHistoryContinuationStore's eventual-consistency (test double).

Build clean, 182/182 OpcUaServer tests pass.
2026-06-15 05:15:07 -04:00
Joseph Doherty 94c3ca60fc feat(historian): server-side continuation-point paging for HistoryRead-Raw
The Wonderware historian backend is single-shot — it returns up to
NumValuesPerNode samples with a null continuation point — so paging is
synthesised server-side, time-based, for the only count-capped arm (Raw):

- A full page (count == NumValuesPerNode, NumValuesPerNode > 0) emits an
  opaque 16-byte continuation point and stores a resume cursor; a short page
  (or NumValuesPerNode == 0 "all values") emits none.
- A resume read takes the stored cursor, reads the next page from the boundary
  forward, and emits a fresh CP only if that page is also full.
- The resume cursor is tie-safe (HistoryPaging.ComputeResumeCursor /
  TrimBoundaryDuplicates): the next page resumes from the boundary timestamp
  INCLUSIVE and drops the head ties already returned, so samples sharing the
  boundary SourceTimestamp are neither duplicated nor skipped.

Continuation points are bound to the OPC UA session via the SDK's
ISession.SaveHistoryContinuationPoint / RestoreHistoryContinuationPoint store
(SessionHistoryContinuationStore) — capped by ServerConfiguration.
MaxHistoryContinuationPoints (default 100, oldest-evicted) and disposed on
session close. releaseContinuationPoints is honoured via an override of
HistoryReleaseContinuationPoints (the base dispatcher routes release-only reads
there, never to the per-details arms). An unknown / evicted / released point
resumes to BadContinuationPointInvalid.

Processed and AtTime stay single-shot: neither details type carries a client
count cap, so the single-shot backend returns the complete result in one read
and there is no "full page" signal to page on (spec-conformant). Modified-value
history remains out of scope.

The pure paging decisions + CP store contract are unit-tested via HistoryPaging
+ InMemoryHistoryContinuationStore; the full multi-page round trip is driven
end-to-end through the node manager with an in-memory store + a series-backed
fake historian (the in-process harness is session-less).
2026-06-15 03:02:48 -04:00
Joseph Doherty a5c0c82661 fix(opcua): address code review on write-outcome surfacing
v2-ci / build (push) Failing after 35s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
- A.1 (false-rejection safety): restrict the structural fail-fast's confident-mismatch check to
  the CLOSED set of built-in types ResolveBuiltInDataType emits (numeric families + Boolean/
  String/DateTime/ByteString). Any other expected type (Enumeration, Guid, …) now defers to the
  SDK, so a coercible write (Int32→Enumeration) is never false-rejected. + A7/A8 regression tests.
- C.1: guard BuildWriteFailureAuditEvent (under Lock) in try/catch like ReportAuditEvent, so a
  SetChildValue surprise is swallowed+logged, never thrown out of the fire-and-forget continuation.
2026-06-15 02:45:51 -04:00
Joseph Doherty bb59fd4e75 feat(opcua): surface failed inbound writes to clients (fail-fast, Bad blip, audit event)
Three deferred 'surface the failed write' enhancements on the write-outcome
self-correction path in OtOpcUaNodeManager:

- Item A: synchronous structural fail-fast. EvaluateEquipmentWriteStructure
  (pure static) rejects a structurally-invalid write INLINE (Bad sync) after
  the authz gate but before the optimistic dispatch, so the SDK never applies
  it. Null payload -> BadTypeMismatch; plus a confidence-gated cheap built-in
  type compatibility check (numeric widening + BaseDataType wildcard tolerated;
  uncertain cases defer to the SDK's own coercion).

- Item B: Bad-quality blip on device-write failure. On a revert,
  RevertOptimisticWriteIfNeeded first publishes the still-applied optimistic
  value with StatusCode BadDeviceFailure, then restores the prior value/status
  (both under the existing Lock). Documents the queue-coalescing caveat (a slow
  subscriber may see only the restored value -> the audit event is the reliable
  signal).

- Item C: Part 8 AuditWriteUpdateEvent on device-write failure. Builds an
  AuditWriteUpdateEventState (SourceNode=node, AttributeId=Value, OldValue=prior,
  NewValue=attempted, ClientUserId from the threaded identity, Message carries
  outcome.Reason) under Lock and reports it via Server.ReportEvent OUTSIDE Lock.
  Guarded so auditing-disabled / report failure never breaks the revert.

Threads the writing identity's user-id + node into the continuation. Adds 6
unit tests for EvaluateEquipmentWriteStructure. Build clean (0 warnings);
158/158 OpcUaServer.Tests green.
2026-06-15 02:38:57 -04:00
Joseph Doherty e6ec0ad8be fix(historian): events arm sets results on bad paths + Variant.Null SourceName + test hardening
- HistoryReadEvents miss path + catch path now both set results[handle.Index] explicitly
  (new SdkHistoryReadResult { StatusCode = BadHistoryOperationUnsupported }) — don't rely on
  base pre-seeding results[i] so every path sets BOTH errors and results coherently (#1)
- ProjectEventField: SourceName null now emits Variant.Null instead of a String-typed null
  variant (evt.SourceName is null ? Variant.Null : new Variant(evt.SourceName)) (#3)
- Comment near the HistoryRead dispatcher block updated: all four arms (Raw/Processed/AtTime
  + Events/Task 4) are now overridden — "left to the base" wording was stale (#5)
- Happy-path test adds ReceiveTime to select clauses and asserts it projects ReceivedTimeUtc
  as a DateTime Variant at the correct select-order position (#4)
- Backend-throw test hardened: asserts errors[0] via ServiceResult.IsBad + explicit code,
  asserts results[0] is non-null with the Bad code (no longer relies on base seeding),
  and asserts EventsEntered to prove the override reached the bridge before the throw (#1)
- RecordingHistorianDataSource gains EventsEntered flag (set before ThrowOnRead check) (#1)
- Events_non_source_node test gains clarifying doc comment explaining the SDK base rejects
  variable nodes (EventNotifier=None) for event reads before our override runs; the
  override's source-guard is exercised by the promoted-without-source test instead (#2)
2026-06-14 20:10:16 -04:00
Joseph Doherty e3c0ef7b41 feat(historian): HistoryReadEvents over equipment-folder notifiers + event-field projection 2026-06-14 19:56:38 -04:00
Joseph Doherty 059f18bdad test(historian): multi-node HistoryRead isolation + single-lookup ServeNode + comment fix
Fix A: add Raw_multi_node_per_node_error_isolation test — two historized variables
(eqA/good→A.PV, eqB/bad→B.PV) in one Raw batch; per-tagname fake throws for B.PV,
returns a sample for A.PV; asserts errors[0]=Good+sample, errors[1]=Bad,
HistoryData[1]=null (no cross-slot leak), no exception escapes.

Fix B: collapse double ConcurrentDictionary lookup in ServeNode — TryGetHistorizedTagname
now captures `out var tagname` on the guard; the resolved tagname is threaded into the
read callback as a second parameter (Func<IHistorianDataSource, string, Task<HistorianRead>>),
removing the redundant ResolveTagname helper (deleted) and the tiny race window between
the check and the second lookup.  All three call-sites (Raw/Processed/AtTime) updated.

Fix C: rewrite the IsReadModified comment at NodeManagerHistoryReadTests.cs:102 — the
SDK's ReadRawModifiedDetails.Initialize() sets m_isReadModified=true (generated ctor body
in Opc.Ua.DataTypes.cs), so the default IS true; the test must explicitly clear it to
false for a plain raw read.  Previous comment said the same thing but imprecisely; now
cites the SDK mechanism (Initialize() call in the public ctor).
2026-06-14 19:44:56 -04:00
Joseph Doherty 13fba8f8fb feat(historian): HistoryRead override (Raw/Processed/AtTime) over IHistorianDataSource 2026-06-14 19:31:39 -04:00
Joseph Doherty 50e1141fc2 test(historian): assert Deferred sink forwards historianTagname + doc nits
I1: DeferredAddressSpaceSinkTests.RecordingSink now captures HistorianTagname
per EnsureVariable call (HistorianQueue/HistorianCalls, matching the
Phase7ApplierTests pattern); new test EnsureVariable_forwards_historianTagname_to_inner_sink
asserts the arg is forwarded unchanged through DeferredAddressSpaceSink.

M1: OtOpcUaNodeManager.EnsureVariable doc-comment notes that a changed
historize intent on an already-registered node is silently ignored until
a RebuildAddressSpace (rebuild precondition for Task 3 implementers).

N2: DeploymentArtifact.ExtractTagHistorize doc wording: "The live-edit
side" → "The live-edit composer side".
2026-06-14 19:16:23 -04:00
Joseph Doherty 6041dc202b feat(historian): materialise historized vars with Historizing + HistoryRead bit + NodeId->tagname map 2026-06-14 19:09:32 -04:00
Joseph Doherty 1d797c1c8a docs(opcua): fix stale NodeWriteRouter reference in EnsureVariable comment
v2-ci / build (push) Failing after 40s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
2026-06-14 01:45:35 -04:00
Joseph Doherty c1e921de0b fix(opcua): RunContinuationsAsynchronously so revert never re-enters the write Lock 2026-06-14 01:39:47 -04:00
Joseph Doherty 10efcf4517 feat(opcua): write-outcome self-correction — capture prior + compare-and-revert on failure 2026-06-14 01:30:20 -04:00
Joseph Doherty bb5832e900 feat(server): inbound operator-write pipeline — OnWriteValue authz gate + node-write router 2026-06-13 12:35:15 -04:00
Joseph Doherty a23fb2b82e feat(server): equipment-tag node writability from Tag.AccessLevel (parity-safe, no migration) 2026-06-13 11:46:00 -04:00
Joseph Doherty 5edea52bd7 docs(galaxy): fix stale SystemPlatform/alias/Galaxy doc comments (review follow-up)
Resolves the code-review notes on 95be607a + the AdminUI bundle: the
EnsureVariable docs (IOpcUaAddressSpaceSink, OtOpcUaNodeManager) and the Tag
entity doc no longer say 'Galaxy / SystemPlatform / alias'; the DriverHostActor
ForwardToMux comment now states the real equipment-tag value-routing gap (the
FullName→NodeId 'live values' milestone) instead of claiming Galaxy values map
straight through.
2026-06-12 22:00:52 -04:00
Joseph Doherty f742050ebd docs(opcua): explain intentional CommentAdded/Retain delta-gate suppression (T20 review)
Three code-review points on commit 004558c2 were correct behavior
that was under-documented, not bugs:

1. AlarmConditionDelta gains explicit paragraphs explaining why
   CommentAdded is absent: it always originates from a client
   AddComment call whose T18 OnAddComment handler returns Good →
   SDK auto-fires the comment event (E2); the engine re-projection
   carries no delta-field change, so the gate correctly suppresses
   the duplicate. Force-firing would double-emit.

2. Same doc explains Retain is intentionally absent: Retain is a
   pure function of Active/Acknowledged (both compared), so it
   cannot flip without a real delta. Notes future risk if that
   ever changes.

3. ReportConditionEvent Time/ReceiveTime comment corrected: the
   projection was already applied by WriteAlarmCondition above
   with identical values; the restamp is a locality repeat, not a
   reorder guard.

Also adds one seam unit-test (103 total, was 102) pinning the
null-vs-empty Message normalization boundary so a change to the
?? string.Empty coalescing is caught at the seam level.
2026-06-11 06:38:31 -04:00
Joseph Doherty 004558c241 fix(alarms): delta-gate WriteAlarmCondition to suppress inbound ack double-emit (T20)
Inbound client acks now route through the engine (T18/T19). On a successful
Acknowledge the T18 gate returns Good, so the SDK applies the acked state to the
AlarmConditionState node and auto-fires its own condition event (E2) -- directly
on the node, bypassing WriteAlarmCondition. The engine then re-projects that same
transition through WriteAlarmCondition, which fired again (E3): a double-emit.

Gate WriteAlarmCondition's ReportConditionEvent on a genuine delta computed
against the node's CURRENT live state (read before projecting the snapshot), not
a last-written cache (which would be stale, since the SDK-applied ack never went
through this method). For a re-projected ack the snapshot equals the node's
already-applied state -> no delta -> suppress E3. Genuine engine-driven
transitions still differ -> fire.

Compared fields (value-equality via AlarmConditionDelta record): Active, Acked,
Confirmed, Enabled, Shelving (mapped from the shelving state machine), Severity
(mapped through MapSeverity to match the bucket the node stores), Message.
Optional Confirmed/Shelving fold to the node read-back default when the child is
absent so they can't register a phantom delta.

Tests prove both: suppression of the simulated inbound ack re-projection
(EventId unchanged) and that genuine transitions fire while identical
re-projections suppress; plus a direct unit test of the ShouldFireConditionEvent
seam. 102/102 OpcUaServer.Tests green.
2026-06-11 06:26:48 -04:00