Commit Graph

70 Commits

Author SHA1 Message Date
Joseph Doherty d4dab75323 v3 B1-WP2: TagConfigIntent sheds FullName identity; RawPaths tests; nodeId key normalization 2026-07-15 18:58:57 -04:00
Joseph Doherty cb720bb8c3 v3 batch1 contracts: RawPaths identity helper + reshaped greenfield entities
Shared-type foundation for Batch 1 fan-out (coordinator contracts commit):
- New Commons/Types/RawPaths.cs — the v3 identity authority (Build/Combine/
  Split/Leaf/TryParent/ValidateSegment, ordinal comparer).
- New entities RawFolder, TagGroup, UnsTagReference.
- Reshape DriverInstance (drop NamespaceId, add nullable RawFolderId),
  Tag (require DeviceId, add nullable TagGroupId, drop DriverInstanceId/
  EquipmentId/FolderPath, keep WriteIdempotent), Equipment (drop
  DriverInstanceId/DeviceId), Device (doc: now universal), ServerCluster
  (nav Namespaces -> RawFolders).
- Delete Namespace, EquipmentImportBatch(+Row), NamespaceKind enum.
- DriverTypeMetadata: drop AllowedNamespaceKinds + NamespaceKindCompatibility.

Solution is intentionally red below the foundation projects until Wave C
integrates. Commons + Core.Abstractions compile green.
2026-07-15 18:52:19 -04:00
Joseph Doherty bbbda99719 feat(browse): Wave-0 Batch F — capture coalescing/cap + AddAdminUI DI registration
Task 10: in-flight coalescing (Lazy<Task<CapturedTree>> keyed on driverType+SHA256(config))
         so repeat Browse clicks share one device walk; global SemaphoreSlim cap
         (MaxConcurrentCaptures=4) queues excess; per-caller ct via WaitAsync leaves the shared
         capture (bounded by open-timeout, CancellationToken.None) unaffected; in-flight-only map
         (Refresh = fresh capture). 7 concurrency tests green (158 total in Commons.Tests).
Task 12: AddAdminUI registers IUniversalDriverBrowser->DiscoveryDriverBrowser with
         NullDriverFactory fallback (standalone AdminUI => CanBrowse false => manual entry).
         AdminUI.Tests 542 green.
2026-07-15 17:37:02 -04:00
Joseph Doherty 698703744f feat(browse): Wave-0 Batch E — UntilStable settle loop + BrowserSessionService fallback
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).
2026-07-15 17:31:50 -04:00
Joseph Doherty fa339a5565 feat(browse): Wave-0 Batch D — IUniversalDriverBrowser + DiscoveryDriverBrowser core
Task 8: IUniversalDriverBrowser + DiscoveryDriverBrowser open path — construct(+PatchForBrowse)
        -> connect -> one-shot capture -> bounded (10s) shutdown; CanBrowse gate (catch-throwing
        TryCreate, defensive teardown of the throwaway instance); deep-merge browse patch.
        Commons gains Microsoft.Extensions.Logging.Abstractions (ILogger doesn't flow transitively)
        + InternalsVisibleTo(Commons.Tests). OTOPCUA0001 suppressed on the deliberate one-shot
        browse-capture DiscoverAsync (not a runtime dispatch path). 12 unit tests green.
2026-07-15 17:27:04 -04:00
Joseph Doherty 15da8d4f0d feat(browse): Wave-0 Batch C — CapturingAddressSpaceBuilder + CapturedTreeBrowseSession
Task 6: CapturedTree/CapturedNode model + CapturingAddressSpaceBuilder — an in-memory
        IAddressSpaceBuilder that records a driver's DiscoverAsync stream (folder nesting,
        leaf FullName as node id, alarm mark, node-cap truncation, no-op alarm sink).
Task 7: CapturedTreeBrowseSession — serves the captured snapshot through IBrowseSession
        (Root/Expand/Attributes from memory, truncation marker, shared-immutable-tree dispose).
13 unit tests green.
2026-07-15 17:22:00 -04:00
Joseph Doherty 85776cf650 feat(browse): Wave-0 Batch A — SupportsOnlineDiscovery gate, Commons ref, TagModal editor plumbing
Task 1: ITagDiscovery.SupportsOnlineDiscovery default member (=> false)
Task 2: Commons -> Core.Abstractions ProjectReference
Task 13: TagModal BuildEditorParameters passes DriverType + GetDriverConfigJson;
         all 7 typed tag editors declare the two new [Parameter]s (DynamicComponent
         throws on unmatched params, so every editor must accept them).
2026-07-15 17:15:32 -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 1a698cbb97 Merge R2-11 TagConfig consolidation (arch-review round 2) [PR #434]
v2-ci / build (push) Successful in 3m55s
v2-ci / unit-tests (push) Failing after 8m14s
Findings 01/C-1, 01/P-1 (single TagConfigIntent.Parse in Commons, 4 copies
delegate, parse-once) + 05 CONV-2/UNDER-1/UNDER-6 (shared strict TagConfigJson
readers, per-driver Inspect() + writable key, FOCAS capability pre-flight,
Modbus probe timeoutMs, deploy-gate Warn|Error). Phase C runtime-strict flip
deferred. T22/T24 deferred. Auto-merged AddressSpaceApplier.cs + DriverHostActor.cs
with R2-04/R2-10; verified OpcUaServer.Tests 286/286 + Runtime.Tests 396/396.
Build clean.
2026-07-13 11:29:32 -04:00
Joseph Doherty d62b140ccd Merge R2-04 Failure-visibility trio (arch-review round 2) [PR #433]
v2-ci / build (push) Successful in 3m48s
v2-ci / unit-tests (push) Failing after 9m14s
Findings 01/S-1 (AddressSpaceApplyOutcome failure field + apply.failed logging,
no optimistic success), 06/S-1 (Galaxy write fails closed -> BadCommunicationError
+ #5 revert, no knowingly-lost raw Write), 03/S4 (PrimaryGatePolicy default-deny
unknown-role-multi-driver on all gates + scripted-alarm emit gate). T13/T15 (2-node
live gates) deferred -- T15 is the behavior-affecting S4 live gate, must run in heavy
pass. Clean merge, build clean.
2026-07-13 11:19:24 -04:00
Joseph Doherty f2e899aaad feat(r2-04): PrimaryGatePolicy - membership-aware default-deny + denial meter (03/S4) 2026-07-13 10:55:46 -04:00
Joseph Doherty 759125372a fix(r2-04): surface degraded address-space applies (Error log + otopcua.opcua.apply.failed) 2026-07-13 10:44:18 -04:00
Joseph Doherty b6633f7562 feat(commons): DeviceConfigIntent — device-host extraction + normalization home (R2-11) 2026-07-13 10:39:54 -04:00
Joseph Doherty 2cb3f962b9 fix(r2-10): DriverResilienceStatusChanged DPS contract 2026-07-13 10:39:18 -04:00
Joseph Doherty cd3f1270ee feat(commons): TagConfigIntent.Parse — single home for the byte-parity TagConfig intents (R2-11, 01/C-1) 2026-07-13 10:39:07 -04:00
Joseph Doherty 7fd44f0fa7 fix(vtags): real script timeout + ALC-safe compile cache (arch-review 02/U2,U3)
U2 (Critical): RoslynVirtualTagEvaluator ran scripts with a CancellationToken
that a synchronous Roslyn script can never observe, so one CPU-bound/infinite-
loop virtual-tag script hung the owning VirtualTagActor forever (no timeout, no
recovery). Route evaluation through the existing TimedScriptEvaluator (Task.Run
+ WaitAsync) so the wall-clock budget is real; a runaway script now returns Bad
within the timeout instead of wedging the actor.

U3 (High): the live path cached evaluators in a raw ConcurrentDictionary never
cleared on republish, leaking a collectible AssemblyLoadContext per edited
script forever. Swap in the purpose-built CompiledScriptCache (Lazy single-
compile, dispose-on-Clear) and add an apply-boundary clear: a new narrow
IScriptCacheOwner seam the VirtualTagHostActor calls on each ApplyVirtualTags
generation (mirrors ScriptedAlarmEngine's per-generation clear).

Tests: infinite-loop-fails-within-timeout (bounded by WaitAsync so a regression
fails instead of hanging), happy-path-after-wrapping, ClearCompiledScripts-
empties-cache, and a host-actor wiring guard asserting each apply generation
calls ClearCompiledScripts (theme #1 production-caller assertion). Host.Integration
VT suite 18/18, Runtime VT suite 12/12.
2026-07-08 16:21:47 -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 34fc304712 fix(otopcua): guard discovered-injection equipment id + cover deferred forwarding 2026-06-26 07:27:09 -04:00
Joseph Doherty 598cdfad5a feat(otopcua): applier pass to materialise discovered nodes idempotently 2026-06-26 07:16:36 -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 6dc74289ce review(Commons): record findings + add deferred-sink/equip-nodeid tests, fix stale Phase7 doc
Code review at HEAD 7286d320. Commons-001 (stale Phase7 telemetry doc) fixed;
Commons-003/004 close test-coverage gaps (DeferredAddressSpaceSink/ServiceLevelPublisher
forwarding seam + EquipmentNodeIds whitespace branch). Commons-002 (CorrelationId
typing) deferred as cross-cutting.
2026-06-19 10:22:59 -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 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 22b0611fb9 fix(opcua): forward ISurgicalAddressSpaceSink through DeferredAddressSpaceSink (F10b surgical path was inert in prod) 2026-06-18 13:57:53 -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 a792820283 feat(opcua): EnsureVariable array params (ValueRank=OneDimension + ArrayDimensions) 2026-06-16 21:16:07 -04:00
Joseph Doherty 069a5f3165 feat(adminui): Galaxy picker pre-fills native-alarm fields from IsAlarm 2026-06-16 16:55:05 -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 418663b359 feat(alarms): thread isNative through MaterialiseAlarmCondition; node manager tracks native conditions [H6a] 2026-06-15 14:13:30 -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 0f7c47a559 feat(commons): IOpcUaNodeWriteGateway + NodeWriteOutcome for write-outcome routing 2026-06-14 01:20:14 -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 26816fd17e feat(commons): EquipmentNodeIds — single source of truth for folder-scoped equipment NodeIds 2026-06-13 06:26:59 -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 2ba2f8a899 feat(commons): TryParseRelayBody — detect pure ctx.GetTag relay scripts 2026-06-11 20:59:10 -04:00
Joseph Doherty 61b230d79a harden(historian): nullable HistorizeToAveva (missing→historize) for rolling-restart-safe deserialize + middle-link test 2026-06-11 13:00:57 -04:00
Joseph Doherty 8012509584 feat(historian): honor per-alarm HistorizeToAveva opt-out at the durable write 2026-06-11 12:48:13 -04:00
Joseph Doherty 8ac3ac5be9 feat(alarms): carry AlarmTypeName + operator Comment on AlarmTransitionEvent (historian feed prep) 2026-06-11 11:03:00 -04:00
Joseph Doherty f9932f2d8e refactor(admin): use CorrelationId wrapper for alarm ack/shelve commands 2026-06-11 09:27:24 -04:00
Joseph Doherty 370a2b7b48 feat(alerts): AdminUI alarm ack/shelve via AdminOperationsActor singleton
T21: add an AdminUI path for acknowledging/shelving alarms that routes
through the admin-pinned AdminOperationsActor cluster singleton, which
republishes onto the same 'alarm-commands' DPS topic the OPC UA method
path (T18) and the engine subscriber (T19) use. The broadcast + the
ScriptedAlarmHostActor ownership filter handle cross-node routing, so
the singleton needs no knowledge of which node owns the alarm.

- Commons: AcknowledgeAlarmCommand/ShelveAlarmCommand (+ result records)
  and a shared AlarmCommandsTopic const; ScriptedAlarmHostActor now
  re-exports that const (mirrors the DriverControlTopic pattern).
- AdminOperationsActor: two handlers map the control-plane messages to
  AlarmCommand (Acknowledge / OneShotShelve / TimedShelve / Unshelve,
  threading User/Comment/UnshelveAtUtc) and publish via the DPS mediator.
- IAdminOperationsClient + AdminOperationsClient: typed Acknowledge/Shelve
  ask wrappers mirroring StartDeploymentAsync.
- Alerts.razor: per-row DriverOperator-gated Ack/Shelve/Unshelve controls;
  operator name from AuthenticationState. Timed-shelve datetime UI deferred.
- 5 TestKit tests (mediator-probe subscribed to alarm-commands) verifying
  each kind's mapping + reply; 56/56 ControlPlane tests green.
2026-06-11 06:44:27 -04:00
Joseph Doherty 1784eedd3f fix(opcua): exempt OnTimedUnshelve from the client AlarmAck gate (system-initiated)
The SDK fires OnTimedUnshelve with the node manager's system context (no
session, no user identity) when a TimedShelve duration expires. Routing
through the shared HandleAlarmCommand hit the AlarmAck gate and returned
BadUserAccessDenied, leaving the alarm permanently shelved.

Replace the delegated HandleAlarmCommand call with an inline lambda that
bypasses the client gate, extracts the AlarmId the same way, and routes an
Unshelve command so the engine clears its shelve state. The manual-client
Unshelve path via OnShelve(shelving:false) remains gated.

Update the AlarmCommandRouterTests OnTimedUnshelve test to use a real
system context (no UserIdentity) — reproducing the actual SDK invocation
path — and assert Good, AlarmId, Operation==Unshelve, User==empty.

Add a doc note to AlarmCommand.Operation that Enable/Disable are in the
vocabulary but not yet wired at the node-manager seam.
2026-06-11 06:16:30 -04:00
Joseph Doherty 63289d377c feat(alarms): route inbound Part 9 alarm methods through AlarmAck gate (T18)
Wire the materialised AlarmConditionState method handlers so a client calling
Acknowledge/Confirm/Shelve/AddComment is gated on the AlarmAck data-plane role
and, when allowed, routed back to the scripted-alarm engine via a new
`alarm-commands` DistributedPubSub topic.

- Commons: new AlarmCommand DTO (AlarmId/Operation/User/Comment/UnshelveAtUtc).
- ScriptedAlarmHostActor: add AlarmCommandsTopic const.
- OtOpcUaNodeManager: settable AlarmCommandRouter + wire OnAcknowledge/OnConfirm/
  OnAddComment/OnShelve/OnTimedUnshelve. Each resolves the principal off
  ISessionOperationContext.UserIdentity as RoleCarryingUserIdentity, fails closed
  (BadUserAccessDenied) when the AlarmAck role is absent or no identity, else maps
  + routes an AlarmCommand and returns Good. OnShelve discriminates OneShotShelve/
  TimedShelve/Unshelve from the SDK flags; TimedShelve expiry = UtcNow + ms.
  No Akka/IActorRef handle — only the Action<AlarmCommand> delegate. T20 de-dup
  note left; WriteAlarmCondition untouched.
- OpcUaServer.Security: OpcUaDataPlaneRoles.AlarmAck shared const (the role was a
  bare string everywhere; introduced one symbol for the gate + tests).
- OtOpcUaSdkServer: SetAlarmCommandRouter pass-through.
- Host: boot wiring publishes each command via mediator.Tell(Publish(...)) using a
  lazy ActorSystem accessor (mirrors DpsScriptLogPublisher).
- Tests: 11 new gate + mapping tests (OpcUaServer.Tests 88->99, all green).
2026-06-11 06:05:39 -04:00
Joseph Doherty 4c417f7fb8 fix(scripted-alarms): log failed event-report via SDK trace + correct sink doc (T16 review) 2026-06-10 19:54:37 -04:00
Joseph Doherty 4eb1d65e2b feat(scripted-alarms): richer AlarmConditionState bridge to the OPC UA node (T15) 2026-06-10 19:41:16 -04:00
Joseph Doherty 60d48a2a0a feat(scripted-alarms): materialise real Part 9 AlarmConditionState nodes (T14) 2026-06-10 19:19:10 -04:00
Joseph Doherty fc0d43a3dc refactor(scripted-alarms): retire orphaned ScriptedAlarmActor + F9b evaluator (T11) 2026-06-10 15:22:26 -04:00
Joseph Doherty 8e8ca9efe8 feat(scripted-alarms): DeploymentArtifact byte-parity for the alarm plan (T6) 2026-06-10 14:41:46 -04:00
Joseph Doherty f431504825 feat(commons): EquipmentScriptPaths — derive base + {{equip}} substitution + shared dep extraction 2026-06-10 07:42:14 -04:00
Joseph Doherty 933dd1a874 feat(audit): OtOpcUa adopt canonical ZB.MOM.WW.Audit.AuditEvent + AuditWriterActor:IAuditWriter + Outcome derivation (Task 2.1)
Deep-adopt the shared audit record. Deletes the bespoke 8-field positional
Commons AuditEvent and repoints the writer path at ZB.MOM.WW.Audit.AuditEvent
(0.1.0, feed-mapped via dohertj2-gitea). Adds the package reference to both
Commons and ControlPlane.

- AuditWriterActor now implements IAuditWriter: WriteAsync(evt, ct) is a
  best-effort, never-throwing entry point that Self.Tell()s the event onto the
  same batching/dedup/flush pipeline and returns Task.CompletedTask. Existing
  Receive<AuditEvent> + 500/5s batching + two-layer dedup unchanged.
- Flush mapping updated for the canonical field types: OccurredAtUtc is now
  DateTimeOffset (.UtcDateTime into the datetime2 column), SourceNode is string?
  (was NodeId.Value), CorrelationId is Guid? (stored null when null). Outcome is
  NOT yet persisted (column lands in Task 2.2).
- New AuditOutcomeMapper.FromAction maps the OtOpcUa action vocabulary to the
  required canonical Outcome: OpcUaAccessDenied / CrossClusterNamespaceAttempt ->
  Denied; config verbs (DraftCreated/Edited, Published, RolledBack, NodeApplied,
  ClusterCreated, NodeAdded, CredentialAdded/Disabled, ExternalIdReleased) ->
  Success. OtOpcUa emits no Failure events.

The Akka message shape changed, but the structured audit path is dormant (zero
production emit/Tell sites; all live audit flows through the bespoke SP path),
so there is no rolling-deploy wire-compat concern. Tested-not-exercised by
design.

ControlPlane.Tests: 44/44 green (AuditWriterActor suite rewritten to construct
the canonical record + assert the Outcome derivation table + the WriteAsync
best-effort/mailbox-routing contract + null SourceNode/CorrelationId handling).
2026-06-02 09:53:12 -04:00
Joseph Doherty 81f09a7054 feat(commons): add IDriverBrowser/IBrowseSession/BrowseNode abstractions 2026-05-28 15:32:01 -04:00