Fourteenth PR of the alarms-over-gateway epic
(docs/plans/alarms-over-gateway.md). Depends on PR B.2 (GalaxyDriver
implements IAlarmSource, merged) and B.3 (DriverNodeManager prefers
driver-native ack, merged).
Three new optional fields on Core.Abstractions.AlarmEventArgs:
- OperatorComment — populated by the driver-native gateway path on
Acknowledge transitions. Null on raise / clear, and null on the
sub-attribute fallback path where the comment collapses into a
single string write.
- OriginalRaiseTimestampUtc — preserved across Acknowledge so OPC
UA Part 9 conditions keep the original raise time.
- AlarmCategory — taxonomy bucket from the upstream alarm system.
Maps to ConditionClassName downstream when a class mapping is
configured.
GalaxyDriver.OnPumpAlarmTransition populates the new fields from
GalaxyAlarmTransition (PR B.1). Empty strings collapse to null so
consumers can use is-null rather than is-null-or-empty checks.
Client.Shared mirror DTO (Client.Shared/Models/AlarmEventArgs)
gains the same three properties so the Client.UI / Client.CLI
surfaces can reflect the rich payload — the actual UI/CLI
verbose-output and Show-Details rendering ship as a follow-up
PR; this PR locks in the payload contract.
Tests:
- 2 new tests in Driver.Galaxy.Tests pin the populated-vs-null
behaviour for full-payload Acknowledge and bare-bones Raise
transitions respectively.
- Solution build clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Thirteenth PR of the alarms-over-gateway epic
(docs/plans/alarms-over-gateway.md). Depends on PR B.2 (GalaxyDriver
implements IAlarmSource, merged).
When DriverNodeManager registers an AlarmConditionState with
AlarmConditionService, it now picks the acknowledger:
- Driver implements IAlarmSource → DriverAlarmSourceAcknowledger
routes the operator comment through IAlarmSource.AcknowledgeAsync
via the existing AlarmSurfaceInvoker (Phase 6.1 resilience pipeline,
no-retry per decision #143). Preserves operator-comment fidelity
end-to-end — the value-driven sub-attribute write collapses the
comment into a single string write that loses MxAccess metadata.
- Driver does not implement IAlarmSource →
DriverWritableAcknowledger fallback (existing behaviour for
AbCip / Modbus / S7 / etc).
The dedup logic that prefers driver-native transitions over
sub-attribute synthesis lives in AlarmConditionService and is
already in place — drivers that surface OnAlarmEvent (B.2) feed
the service directly, while sub-attribute writes still flow
through DriverNodeManager's ConditionSink so a Galaxy template
without $Alarm extensions stays functional.
Tests:
- 2 new routing-decision tests in
DriverAlarmSourceAcknowledgerRoutingTests pin the
IAlarmSource detection used at registration time.
- Server build clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Twelfth PR of the alarms-over-gateway epic
(docs/plans/alarms-over-gateway.md). Depends on PR B.1 (EventPump
dispatch, merged) and PR E.2 (.NET SDK alarm methods, merged).
Restores the v1 IAlarmSource capability that PR 7.2 retired with the
legacy Galaxy.Host / Galaxy.Proxy projects.
GalaxyDriver gains:
- IAlarmSource on the class declaration → eight capabilities total
(IDriver / ITagDiscovery / IReadable / IWritable / ISubscribable /
IRediscoverable / IHostConnectivityProbe / IAlarmSource).
- SubscribeAlarmsAsync — returns a sentinel handle and starts the
shared EventPump (alarm wiring is lazy on first sub).
Multiple handles share the same gateway stream; the server-side
AlarmConditionService dispatches per-source-node downstream.
- UnsubscribeAlarmsAsync — symmetric handle removal; rejects
handles not issued by this driver.
- AcknowledgeAsync — issues one gateway RPC per acknowledgement
through IGalaxyAlarmAcknowledger. ConditionId carries the alarm
full reference; falls back to SourceNodeId when empty.
- OnAlarmEvent — bridges EventPump.OnAlarmTransition (B.1) onto
AlarmEventArgs. Suppressed when no alarm subscription is active so
untracked transitions don't leak through.
New runtime types:
- IGalaxyAlarmAcknowledger — test seam.
- GatewayGalaxyAlarmAcknowledger — production wrapper around
MxGatewayClient.AcknowledgeAlarmAsync (PR E.2). Maps native
MxStatus failures to a logged warning rather than a thrown
exception so a transient MxAccess hiccup doesn't fail the
operator's Acknowledge.
- GalaxyAlarmSubscriptionHandle — driver-side IAlarmSubscriptionHandle.
Production runtime construction in BuildProductionRuntimeAsync wires
the acknowledger when not pre-injected; tests inject a fake via the
internal ctor.
Tests:
- 7 new tests in GalaxyDriverAlarmSourceTests — subscribe → event
fire path, suppress without subscription, unsubscribe stops flow,
foreign-handle rejection, ack routes per-request, ack falls back
to SourceNodeId, ack throws NotSupported without acknowledger.
- Full Driver.Galaxy.Tests: 203 passed (was 196; 7 new).
Operates as a "stub-ready" surface — runtime ack calls will return
PERMISSION_DENIED until A.3 ships the gateway-side dispatch, and no
alarm transitions will arrive until A.2 adds the worker MxAccess
subscription. Both will activate this code path automatically when
the gateway side lands.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sixth PR of the alarms-over-gateway epic
(docs/plans/alarms-over-gateway.md). Depends on PR C.2 (sidecar
serves IAlarmEventWriter when enabled), already merged.
Today Phase7Composer.ResolveHistorianSink only scans drivers for an
IAlarmHistorianWriter — no Galaxy driver provides one since PR 7.2,
so the resolution falls through to NullAlarmHistorianSink and
scripted-alarm transitions are silently discarded.
WonderwareHistorianClient already implements IAlarmHistorianWriter
and Program.cs:178 already registers it as a singleton when
Historian:Wonderware:Enabled=true. The gap was that Phase7Composer
ignored DI: this PR adds an optional injectedWriter constructor
parameter, and ASP.NET Core DI resolves it from the same
registration when present.
- Phase7Composer constructor: new optional IAlarmHistorianWriter?
injectedWriter parameter (default null). Backward-compatible —
existing callers don't need to change; DI populates it
automatically when the singleton is registered.
- New static SelectAlarmHistorianWriter helper — resolution order
is driver → DI → null. Drivers win when both are present so a
future GalaxyDriver-as-IAlarmHistorianWriter takes the write
path directly, preserving the v1 invariant where a driver that
natively owns the historian client doesn't bounce through the
sidecar IPC.
- ResolveHistorianSink uses the helper + emits a structured log
line identifying which source provided the writer.
Tests:
- 4 SelectAlarmHistorianWriter precedence tests — no source / DI
only / driver wins over DI / first-driver-with-writer wins.
- Pre-existing 4 HostStatusPublisherTests SQL failures unrelated
to this change (require the docker-host SQL Server at
10.100.0.35,14330 per CLAUDE.md). Phase7 + alarm tests all
green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fifth PR of the alarms-over-gateway epic
(docs/plans/alarms-over-gateway.md). Depends on PR C.1
(AahClientManagedAlarmEventWriter), already merged.
Today HistorianFrameHandler is constructed at Program.cs line 57
without an alarmWriter, so every WriteAlarmEvents frame replies
"Sidecar not configured with an alarm-event writer" and the lmxopcua
side keeps the row queued. C.2 wires a real writer behind a new
OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED toggle.
- Program.BuildAlarmWriter — gated on the env var (default true,
fail-open under accidental misconfiguration). Constructs an
AahClientManagedAlarmEventWriter wrapping a
SdkAlarmHistorianWriteBackend with the same connection config the
read path uses.
- Install-Services.ps1 — appends OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED=true
to the OtOpcUaWonderwareHistorian service env block when the
sidecar is installed. Read-only deployments flip it to false at
service-config edit time without re-installing.
- HistorianFrameHandler signature already accepts
IAlarmEventWriter? — supplying non-null at line 57 lights up
the WriteAlarmEvents reply path that's been dormant since PR 3.3.
Until PR D.1 pins the live aahClientManaged entry point, the
SdkAlarmHistorianWriteBackend reports RetryPlease for every event
with a structured diagnostic. The lmxopcua-side
SqliteStoreAndForwardSink retains queued events; same effective
behaviour as today's NullAlarmHistorianSink fallback but with
visible diagnostics rather than silent discard.
Tests:
- 6 BuildAlarmWriter env-var cases — unset / true / false /
unrecognized → default-on / capitalization variants.
- Full sidecar test suite: 56 passed (was 48; 8 new).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fourth PR of the alarms-over-gateway epic
(docs/plans/alarms-over-gateway.md). Independent of Tracks A and B —
the sidecar slot defined in HistorianFrameHandler line 242 is unwired
today; PR C.2 (next) flips it on in Program.cs.
- AlarmHistorianWriteOutcome (sidecar-local, net48 — twin of
Core.AlarmHistorian.HistorianWriteOutcome which is net10): Ack /
RetryPlease / PermanentFail.
- IAlarmHistorianWriteBackend abstraction so the SDK call can be
faked in unit tests.
- AahClientManagedAlarmEventWriter implements IAlarmEventWriter,
delegates to the backend, maps Ack→true / Retry|Permanent→false
for the IPC bool[] reply contract. Backend exception → whole
batch RetryPlease (preserves the sender's queue across transients
rather than dropping). Wrong-count return defends against a
backend bug desyncing queue accounting.
- SdkAlarmHistorianWriteBackend — production binding skeleton.
Reports RetryPlease for every event and logs a structured
diagnostic until PR D.1 pins the live aahClientManaged entry
point against the dev rig. The sender's SqliteStoreAndForwardSink
retains queued events, mirroring today's NullAlarmHistorianSink
behaviour but with visible diagnostics instead of silent discard.
- MapOutcome shared helper — pinned via theory tests so the D.1
swap can change the SDK call site without reshuffling the
HRESULT → outcome mapping.
Tests:
- 6 writer tests — empty batch / single Ack / mixed Ack-Retry-
Permanent-Ack ordering / backend-throw → RetryPlease batch /
cancellation propagates / wrong-count defensive degrade.
- 5 outcome theory cases — hresult 0 → Ack, malformed wins over
hresult 0, comm error → Retry, unknown failure → Retry,
malformed + comm → Permanent.
- Full sidecar test suite: 48 passed (was 42; 6 new).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Second PR of the alarms-over-gateway epic
(docs/plans/alarms-over-gateway.md). Depends on PR A.1 in mxaccessgw
(merged) which added the OnAlarmTransitionEvent body + family. No
runtime impact yet — the gateway doesn't emit the new family until
A.3 ships; this PR just stops dropping it on the floor.
EventPump.Dispatch becomes a switch on MxEventFamily. The new
DispatchAlarmTransition decodes the proto event, runs the raw severity
through MxAccessSeverityMapper (the same four-bucket ladder v1 used —
250/500/750/1000 boundaries per docs/v1/AlarmTracking.md), and fires
an internal OnAlarmTransition event with a GalaxyAlarmTransition
record carrying the full payload.
Body absent or transition-kind unspecified → counted via
galaxy.alarm_transitions.decoding_failures and dropped. Gateway
version skew or worker malformed event therefore degrades to "fall
back to the sub-attribute path" rather than crashing the pump.
GalaxyDriver consumes the internal event in PR B.2 (next), wrapping
it onto IAlarmSource.OnAlarmEvent. The richer fields (operator user
+ comment, original raise time, category) become visible on the OPC
UA Part 9 condition once AlarmEventArgs gets extended in E.7.
Tests:
- MxAccessSeverityMapperTests — full bucket ladder + clamp behaviour
for negative + out-of-range inputs.
- EventPumpAlarmTests — raise/ack/clear sequence dispatches in order
with operator metadata + original-raise preserved; unspecified
kind drops; missing body drops; mixed data-change + alarm streams
dispatch independently; OnWriteComplete / OperationComplete
filtered out.
Full Driver.Galaxy.Tests suite: 196 passed (was 191 — 5 new tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cover both client surfaces that become user-visible when the alarm
path lights up:
- mxaccessgw client SDKs in 5 languages (.NET, Python, Go, Java,
Rust). E.1 regens proto across all of them; E.2-E.6 add per-language
alarms helpers (subscribe / acknowledge / query-active) plus matching
CLI verbs.
- lmxopcua OPC UA-facing clients (Client.CLI, Client.UI). E.7 extends
AlarmEventArgs with the new optional fields, surfaces them in the
CLI's --verbose / --json output and the UI's Show-details toggle,
and updates ClientRequirements + Client.{CLI,UI}.md.
Sequencing: E.1 first (mechanical regen), then E.2-E.7 in parallel.
E.2 (.NET) is on the critical path because lmxopcua consumes it; the
other-language SDKs can ship asynchronously without gating D.1.
12 PRs grew to 19 total: 4 in A, 5 in B, 2 in C, 7 in E, 1 in D.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After A/B/C all merge, the running services on C:\publish need to be
refreshed before the Galaxy alarm-event family flows end-to-end. Add
PR D.1: a Refresh-Services.ps1 script + runbook for stopping in
reverse-dependency order, restaging binaries from the build outputs,
restarting in forward-dependency order, and capturing a smoke-run
artifact.
D.1 gates B.5 (docs sweep) — the documentation records the
as-deployed shape, so the deployment has to be live first.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Revise the alarms-over-gateway plan based on review feedback:
The gateway is for MxAccess (live data + Galaxy hierarchy); the
Wonderware historian sidecar is for aahClientManaged (time-series +
alarms historian). Two SDKs, two concerns. Routing alarm-historian
write-back through the gateway would force coupling that doesn't need
to exist — the sidecar already has a dormant WriteAlarmEvents IPC slot
ready to wire.
Drop A.5 (gateway WriteHistorianEvent RPC). Add Track C — two PRs in
the historian sidecar that complete the dormant slot:
C.1 AahClientManagedAlarmEventWriter implementation
C.2 Program.cs wires the writer into HistorianFrameHandler
B.4 reverses from "delete the IPC slot" to "consume the IPC slot" via
a new SidecarAlarmHistorianWriter on the lmxopcua side.
Also tightens Why-section #3 + D5 to make explicit that the path is
exclusively for non-Galaxy alarm producers (scripted alarms today, AB
CIP ALMD or others future). Galaxy-native alarms reach AVEVA Historian
via System Platform's own HistorizeToAveva toggle, independent of
anything in our stack.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Coordinated cross-repo epic to restore the three v1 alarm capabilities
that PR 7.2 regressed: rich MxAccess alarm-event metadata, native
Acknowledge semantics, and the IAlarmHistorianWriter write-back path.
Architectural split: gateway owns MxAccess transport (new
OnAlarmTransition event family + AcknowledgeAlarm / QueryActiveAlarms /
WriteHistorianEvent RPCs); lmxopcua keeps the OPC UA Part 9 state
machine, ACL/role enforcement, and multi-source aggregation. The
existing value-driven sub-attribute path stays as fallback.
10 PRs total — 5 in mxaccessgw, 5 in lmxopcua — sequenced so each
side's work is independently reviewable. End-of-epic gate is a parity
matrix run with five new alarm scenarios.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The sidecar was set to PlatformTarget=x86 + Prefer32Bit=true to mirror
v1's Driver.Galaxy.Host bitness, which itself was x86 only because of
MXAccess COM. PR 7.2 retired Galaxy.Host, so that constraint is gone.
AVEVA Historian 2020 ships an x64 build of every SDK assembly the
sidecar needs (lib\aahClientManaged.dll + aahClient.dll + aahClientCommon.dll
sourced from C:\Program Files (x86)\Wonderware\Historian\x64\; the
remaining three SDK assemblies — Historian.CBE / DPAPI /
ArchestrA.CloudHistorian.Contract — are pure-managed AnyCPU and load
in either bitness). Drop PlatformTarget to x64 on both the sidecar
project and its test project; running 37/37 historian tests + the
live install confirms the SDK loads and serves the named pipe in a
64-bit-native process.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Microsoft.NET.Sdk doesn't auto-include appsettings.json the way Web SDK
does, so dotnet publish was leaving it behind. Without it next to the
EXE the Windows-service-mode host can't find Node + ConfigDb config and
the install scripts had to copy it by hand.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Delete _p54.json / _p55.json (PR-body snapshots for the shipped S7
+ Mitsubishi research docs).
- Delete session.dat (38-byte CLI runtime cache, not produced by any
current source code) and add it to .gitignore so it doesn't come
back.
- Delete lmx_backend.md / lmx_mxgw.md / lmx_mxgw_impl.md. All three
carried "✅ Completed 2026-04-30" historical-record banners — the
v2-mxgw migration shipped + merged to master, so the design plans
served their purpose. Drop the cross-refs from CLAUDE.md and
docs/v1/README.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- docs/drivers/FOCAS.md and docs/v2/implementation/focas-wire-protocol.md
pointed at focas-deployment.md and focas-simulator-plan.md, both of
which were untracked drafts that have since been removed. Drop the
refs (the wire-protocol companion now stands on its own; deployment
guidance lives inline in the FOCAS driver doc).
- Link the orphan v2 design docs from docs/README.md (multi-host
dispatch, v2 release readiness, the historical lmx-followups tracker)
and from modbus-test-plan.md (s7.md, mitsubishi.md per-family quirk
catalogs, sibling to dl205.md).
Surfaced by the doc audit; no content changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- gr/ folder moved to sibling repo at C:\Users\dohertj2\Desktop\graccess\gr;
the SQL queries + DDL captures belong with the graccess CLI work, not
with the OPC UA server. PR 7.2 retired direct Galaxy-DB access from this
repo (mxaccessgw owns those queries server-side now).
- Drop the now-obsolete "Galaxy Repository Database" section in CLAUDE.md
for the same reason — server no longer queries the DB directly.
- Delete root scratch files surfaced by the doc audit (runtimestatus.md,
service_info.md) — abandoned plan + operational scratch.
- Delete docs/v2/implementation/pr-{1,2,4}-body.md — ephemeral PR-body
snapshots from the v2-mxgw rollout.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two follow-ups from the post-PR-7.2 audit:
1. Reinstate verified-current architecture deep-dive links that the
doc-cleanup pass dropped pending verification:
- docs/OpcUaServer.md (server composition, namespace fan-out,
Polly invoker)
- docs/IncrementalSync.md (driver-backend rediscovery + config
publishes)
- docs/ReadWriteOperations.md (driver vs virtual vs scripted-alarm
dispatch)
All three reference live Phase 6.2 / Phase 7 features and the
current GenericDriverNodeManager / CapabilityInvoker / OTOPCUA0001
analyzer codepaths.
2. Restructured the README link table into three logical sections —
"Architecture deep-dives" / "Drivers" / "Clients" — and added a
"v1 archive" section pointing at docs/v1/ for the retired in-process
MXAccess docs.
3. Removed the dead docs/Configuration.md link (the file moved to
docs/v1/Configuration.md in the v1 archive sweep). All 16 link
targets in the new README now resolve.
Plus: physically removed the 9 leftover Driver.Galaxy.* directories
from src/ and tests/ that PR 7.2's git rm cleared from tracking but
left as orphan bin/obj scaffolding on disk. No tracked-content
change for that part.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit (three parallel agent passes) found 43 markdown files carrying
stale references to the deleted Galaxy.Host/Proxy/Shared projects
after the v2-mxgw merge. This commit lands the prioritized fixes.
Track 1 — high-traffic in-place rewrites (3 files, ~454 lines deleted)
- README.md (202 → 91 lines): drops .NET 4.8 / x86 / TopShelf install
text; leads with the multi-driver .NET 10 server identity and points
at scripts/install/Install-Services.ps1 and the parity rig.
- docs/v2/driver-specs.md §1 Galaxy (~289 → ~66 lines): replaces the
Tier-C out-of-process spec with a Tier-A in-process description
matching the current GalaxyDriver code, with the four-section
GalaxyDriverOptions JSON shape pulled verbatim from
Config/GalaxyDriverOptions.cs.
- docs/drivers/Galaxy.md (211 → 92 lines): full rewrite around the
current Browse/Runtime/Health/Config sub-folders.
Track 2 — historical banners (5 files)
- lmx_mxgw.md, lmx_mxgw_impl.md, lmx_backend.md,
docs/v2/Galaxy.ParityMatrix.md,
docs/v2/implementation/phase-2-galaxy-out-of-process.md each get a
"✅ Completed 2026-04-30 — historical record" banner block. lmx_mxgw.md
also fixes two dead links (`docs/Galaxy.Driver.md` and
`docs/v2/Galaxy.Driver.md`) → `docs/drivers/Galaxy.md`.
Track 3 — v1 archive sweep (10 git mv + 1 new index + 2 in-place scrubs)
- Moved 10 v1 docs under docs/v1/ preserving subpath structure:
AlarmTracking, Configuration, DataTypeMapping, HistoricalDataAccess,
Subscriptions (top-level); drivers/Galaxy-Repository,
drivers/Galaxy-Test-Fixture; reqs/GalaxyRepositoryReqs,
reqs/MxAccessClientReqs, reqs/ServiceHostReqs.
- New docs/v1/README.md is the shared archive banner + per-file table.
- docs/README.md repointed to the v1 paths and updated to reflect the
v2 two-process deploy shape (Server + Admin + optional
OtOpcUaWonderwareHistorian).
- docs/v2/Galaxy.ParityRig.md got a historical banner + four inline
scrubs marking the OtOpcUaGalaxyHost service / Driver.Galaxy.Host
EXE / Driver.Galaxy.ParityTests project as deleted-in-PR-7.2.
The repo's live-reading surface (README + CLAUDE.md + docs/v2/) now
describes only the post-PR-7.2 architecture. v1 docs are preserved as
a labelled archive under docs/v1/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the v2-mxgw migration's housekeeping debt now that PR 7.2 has
retired the legacy projects + service.
Repo docs:
- CLAUDE.md: rewrote the Galaxy section + reference-impl + MXAccess
documentation pointers; replaced .NET 4.8 x86 / COM apartment
constraints with .NET 10 AnyCPU + a pointer to the gateway. Dropped
the "Service hosting (Galaxy.Host)" library-preferences row.
- docs/ServiceHosting.md: rewrote (was 156 lines of Galaxy.Host pipe
IPC details). Now reflects the v2 process shape: OtOpcUa.Server +
OtOpcUa.Admin + optional OtOpcUaWonderwareHistorian, with Galaxy
access via the in-process driver → mxaccessgw.
- docs/v2/dev-environment.md: scrubbed four Galaxy.Host references
(TwinCAT/Galaxy.Host shared-host note; .NET 4.8 SDK row; install
step #2; risks table). The .NET 4.8 SDK is now correctly framed as
"optional, only needed when building the mxaccessgw worker".
- mxaccess_documentation.md: deleted from the repo root (obsolete; the
gateway repo is the canonical MxAccess API doc).
Memory housekeeping (under ~/.claude/projects/.../memory/):
- Retired: project_galaxy_host_service.md,
project_galaxy_host_installed.md, reference_impl.md (the LmxProxy
Host MXAccess reference is no longer the design pattern this repo
uses).
- Revised: project_overview.md (now describes the .NET 10 + mxaccessgw
shape), project_aveva_platform_installed.md (AVEVA still required
on the dev box but consumed by the gateway worker, not by anything
here), project_galaxy_via_mxgateway.md (post-7.2 state — flagged as
the only Galaxy backend), project_server_history_alarm_subsystems.md
(per-driver fallbacks retired in PR 7.2).
- MEMORY.md index updated to match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Matrix-gate satisfied (14 passed / 1 skipped / 0 failed on 2026-04-30
per docs/v2/Galaxy.ParityMatrix.md). Galaxy access flows through the
in-process GalaxyDriver → mxaccessgw exclusively. Legacy infrastructure
deleted in this commit:
Source projects (6):
- src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host (.NET 4.8 x86 + MXAccess COM)
- src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy (in-process pipe client)
- src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared (pipe-IPC contracts)
- tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests
- tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests
- tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared.Tests
Test projects with no consumer after legacy retired (3):
- tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.E2E (drove Galaxy.Host EXE)
- tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.ParityTests (drove both backends)
- tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.TestSupport (only consumed by Host/Proxy tests)
Edits:
- ZB.MOM.WW.OtOpcUa.slnx: drop nine project entries
- Server.csproj: drop Driver.Galaxy.Proxy ProjectReference
- Server/Program.cs: drop GalaxyProxyDriverFactoryExtensions.Register
+ the parallel-registration comment block; only GalaxyDriverFactoryExtensions
registers now under DriverType "GalaxyMxGateway"
- Install-Services.ps1: rewrite to drop OtOpcUaGalaxyHost service install +
the GalaxySharedSecret/ZbConnection/GalaxyClientName/GalaxyPipeName/
AvevaServiceDependencies/MxAccessInitialConnect* parameters that only
applied to the legacy host. Adds a closing note pointing operators at
the separate mxaccessgw install
- Uninstall-Services.ps1: keep OtOpcUaGalaxyHost in the cleanup loop so
pre-7.2 rigs upgrade-uninstall cleanly, plus add OtOpcUaWonderwareHistorian
- scripts/e2e/test-galaxy.ps1: deleted (drove the legacy E2E)
- scripts/e2e/e2e-config.sample.json: rewrite the galaxy section comment
to reflect the GalaxyMxGateway-only path
- scripts/e2e/README.md: drop OtOpcUaGalaxyHost references
- scripts/compliance/phase-7-compliance.ps1: drop Galaxy.Shared
HistorianAlarms* checks (those contracts moved to
Driver.Historian.Wonderware.Client in PR 3.4)
Live state: OtOpcUaGalaxyHost Windows service stopped + removed via
NSSM before this commit. The dev box's Galaxy access is now exclusively
through the running mxaccessgw (separate repo).
Stays out of scope for PR 7.2 (PR 7.3 territory):
- CLAUDE.md Galaxy section rewrite
- mxaccess_documentation.md deletion
- Memory entries for the now-retired Galaxy.Host service
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The parity matrix gate is the precondition for retiring the legacy
Galaxy projects. The 24h × 50k soak run and 2-week production pilot
were sketched in early planning as additional safety nets but aren't
operationally applicable for this deployment — there's no separate
production fleet to pilot against, and the soak harness's value is as
ongoing diagnostic infrastructure (still shipped in PR 6.4) rather
than a one-shot release gate.
PR 7.2's only remaining precondition is the matrix being fully green
or carrying documented accepted-deltas — verified 2026-04-30 on the
dev rig: 14 passed / 1 skipped / 0 failed.
Affected:
- docs/v2/Galaxy.ParityMatrix.md "Outstanding deltas" — flips to
"PR 7.2 is unblocked"
- docs/v2/Galaxy.ParityRig.md "After the rig is green" — drops the
three-step soak+pilot flow, keeps only the matrix-doc bookkeeping
follow-up
- lmx_mxgw_impl.md PR 7.2 "Depends on" — replaces "fully soaked"
with the matrix-green precondition + the verification date
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-to-end run on the live ZB galaxy with mxaccessgw on
http://localhost:5120: 14 passed / 1 skipped / 0 failed in 18m53s.
PR 7.2's matrix-gate condition met. Three resolution patches in this
commit; the matrix doc records the new state.
1. Discoverer: defensive `[]` array-suffix strip
----------------------------------------------------
The gw's GalaxyRepository.cs:173-175 appends `[]` to
array-typed full_tag_reference values, but MxAccess COM
IInstance.AddItem doesn't accept `[]`-suffixed addresses.
GalaxyDiscoverer.StripArraySuffix removes the suffix client-side
so SubscribeBulk / Read / Write paths see the canonical form.
Tracked in mxaccessgw/requirements-array-suffix-fix.md; this
workaround is removed when the gw fix lands.
2. WriteByClassification: pin status class, not exact code
---------------------------------------------------------
Legacy MxAccessGalaxyBackend.WriteValuesAsync flat-maps every
failure to BadInternalError (0x80020000); mxgw's
GatewayGalaxyDataWriter.TranslateReply uses
MxStatusProxy.RawDetectedBy to distinguish gw-layer faults
(BadCommunicationError, 0x80050000) from MxAccess HRESULT
faults. Both yield Bad-status — the parity invariant is the
status class (Good/Uncertain/Bad), not the exact code. Both
write tests now use AssertStatusClassMatches; legacy mapping
retires alongside GalaxyProxyDriver in PR 7.2.
3. BrowseAndReadParity Read scenario: drop CLR-type assertion
------------------------------------------------------------
Legacy returns the raw VARIANT (e.g. byte[]) for an attribute
that hasn't received its first value cycle from MxAccess yet,
while mxgw returns the typed value (Single, Int32, etc.). Once
a real value is written or scanned, both converge. Pinning
CLR-type equality across the uninitialized window adds noise
without a real parity invariant — the StatusCode-class
assertion already covers the "did the read succeed" question.
The test still pins StatusCode-class parity per scenario.
4. Galaxy.ParityMatrix.md — first-rig results captured
-----------------------------------------------------
Per-row status flipped from "n/a unverified" to actual
green / yellow / deferred outcomes from this run. Four new
accepted-deltas added (read-value CLR type, write-status code
mapping, single-platform ScanState scope, gw `[]` suffix
workaround), bringing the total to nine. Outstanding deltas
section flipped to "none as of 2026-04-30."
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After running the matrix end-to-end against the live rig for the
first time, three of the nine failures were false positives — bugs in
the harness and test invariants, not real backend deltas:
1. ParityHarness configured the legacy backend with
OTOPCUA_GALAXY_BACKEND=db, which is Discover-only. Reads, writes,
and reinits all returned "MXAccess code lift pending — DB-backed
backend covers Discover only". Switched to mxaccess backend; the
ZB connection string still drives the discovery path.
2. HistoryReadParityTests asserted "neither backend implements
IHistoryProvider" — but the legacy GalaxyProxyDriver still does
(it's an accepted back-compat delta retired in PR 7.2). The
architectural pin we *want* is "the new path doesn't regress to
per-driver history", so the test now asserts only the mxgw side.
3. AlarmTransitionParityTests strict-pinned the five sub-attribute
refs (InAlarmRef, etc.) on the legacy condition. PR 2.1 added
those refs specifically so the new mxgw driver could populate them
via AlarmRefBuilder; legacy pre-dates PR 2.1 and leaves them null
— that's correct, not a regression. Test now asserts a one-way
invariant: when legacy populated a ref, mxgw must match. When
legacy is null, mxgw is free to populate (the mxgw → server-side
AlarmConditionService direction).
The six remaining failures are real:
- 2 from the gw-side `[]` array suffix (filed in
mxaccessgw/requirements-array-suffix-fix.md)
- 2 write-StatusCode mapping deltas (0x80050000 vs 0x80020000) —
Bad-status both ways but mapped to different OPC UA codes
- 1 event-rate ratio of 5x (mxgw dispatches 5x legacy in the same
3s window)
- (Plus the 2 ScanState scenarios that skip cleanly — single-platform
rig as documented)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the placeholder "configure an API key per gateway.md" with
the actual commands that worked end-to-end on this dev box:
- Build both halves (Worker x86 net48, Server net10)
- apikey init-db + apikey create-key with the seven scopes the parity
test exercises (session:*, invoke:*, events:read, metadata:read)
- Three env-var overrides at server startup — capturing real lessons
learned standing the rig up:
* Kestrel__Endpoints__Http__Url = http://localhost:5120
* Kestrel__Endpoints__Http__Protocols = Http2 (gRPC needs h2c on
plain HTTP — without this flag the client gets HTTP_1_1_REQUIRED)
* MxGateway__Worker__ExecutablePath = absolute path to the built
worker (appsettings.json's relative path drops \net48 and the
server can't resolve it)
- Note that workers spawn lazily on first OpenSession, not at server
startup — so port-listening is necessary but not sufficient
evidence the gateway is healthy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Calls out the single-platform constraint on this dev box and the
graccess-cli at C:\Users\dohertj2\Desktop\graccess as the way to
configure the rest of the parity-rig Galaxy shape:
- ScanState probe parity (multi-platform) is deferred to a customer
rig — not feasible on this dev box. PR 7.2 gate accepts
"n/a, deferred" on those rows because PR 4.7's unit tests already
pin the state-decoder + member-tracking logic.
- Per-row provisioning recipes for the five ⚙-scriptable rows:
FreeAccess/Operate UDA, Configure/Tune UDA, value-change source
(recommend external write-loop over template surgery), $Alarm*
extension, History extension. All against a reserved
OtOpcUaParityTest sandbox UDO so plant-relevant objects stay
untouched.
- Trailing deploy + Galaxy.Host restart so MxAccess picks up the
change before re-running the matrix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Walks through standing up both Galaxy backends side-by-side against a
single live Galaxy:
- Conceptual layout (two MxAccess sessions on distinct ClientNames so
they don't evict each other)
- What's already on the dev box (AVEVA + OtOpcUaGalaxyHost service)
- mxaccessgw build + run + config (API key, ClientName)
- The three OTOPCUA_PARITY_* env vars the harness reads
- HarnessShapeTests as the two-line truth-teller for "did both halves
resolve"
- Galaxy-shape coverage matrix mapping each scenario to what's needed
for it to assert (rather than skip)
- Soak run recipes, including the compressed-tag fallback when the dev
Galaxy doesn't have 50k attributes
- Troubleshooting for the four common SkipReasons
- Three further gates before PR 7.2 lands (matrix green, soak data,
pilot flip)
Explicitly drops the stale "use a non-elevated shell" precondition —
the legacy Galaxy.Host pipe ACL accepts elevated and non-elevated
dohertj2 alike (resolved 2026-04-24).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lands the five concrete code-level follow-ups identified after Phase 7.1:
#1 GalaxyDriver.ReadAsync now works in production. Previously threw
NotSupportedException when no test reader was injected. New path
subscribes through the existing SubscriptionRegistry + EventPump,
waits for the first OnDataChange per item handle (gw pushes the
initial value after SubscribeBulk), then unsubscribes. Tags the gw
rejects up front, or that don't publish before the caller's CT
fires, return Bad-status snapshots in input order so callers still
get one snapshot per requested reference.
#2 ResolveApiKey() routes Gateway.ApiKeySecretRef through three forms:
env:NAME, file:PATH, or literal-string fallback. A future DPAPI arm
slots in here without touching the call site.
#3 GatewayGalaxySubscriber actually honors bufferedUpdateIntervalMs now
(was being silently dropped). Calls SetBufferedUpdateInterval via
the gw's MxCommandKind.SetBufferedUpdateInterval before SubscribeBulk
when the requested interval differs from the cached last-applied
value. Soft-fails on a non-Ok protocol status (the SubscribeBulk
still succeeds at gw cadence).
#4 GalaxyMxAccessOptions.EventPumpChannelCapacity surfaces the bounded-
channel size through DriverConfig JSON, defaulting to 50_000.
#5 Stale doc-comments in HostStatusAggregator and GatewayGalaxySubscriber
describing follow-ups that already shipped.
Tests: +6 (read subscribe-once happy path + rejected-tag fallback;
five resolver scenarios). Total Galaxy driver tests now 180/180 green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Forward-looking doc surface for the new in-process GalaxyDriver:
- CLAUDE.md gains a "v2 Galaxy backend" preamble at the top pointing
readers at lmx_mxgw.md and docs/v2/Galaxy.Performance.md, and
framing the rest of the doc as the still-accurate v1 Galaxy.Host
description.
- New auto-memory entry project_galaxy_via_mxgateway.md captures the
default-since-PR-7.1 status, perf surface entry points, and the
soak validation knobs.
Intentionally deferred until PR 7.2 (parity-rig-validated):
- Removing the v1 description and rewriting the architecture section
outright.
- Deleting mxaccess_documentation.md (still consumed by Galaxy.Host).
- Retiring memory entries for project_galaxy_host_service.md /
project_galaxy_host_installed.md / project_aveva_platform_installed.md
— those describe a stack that's still installed and in active use.
- Scrubbing Galaxy.Host references from docs/v2/dev-environment.md,
docs/ServiceHosting.md, docs/Redundancy.md, docs/security.md.
All those changes presuppose the legacy stack is gone, which it isn't
yet. Re-open this PR's tail once the parity matrix in
docs/v2/Galaxy.ParityMatrix.md is fully green on a live rig.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Galaxy.DefaultBackend = "GalaxyMxGateway" to the server
appsettings as the forward-looking default for tooling and migration
scripts that author new Galaxy DriverInstance rows. No runtime
behavior change — both factories register independently at startup,
so existing rows keep working until PR 7.2 retires the legacy
registration (gated on the parity matrix in
docs/v2/Galaxy.ParityMatrix.md going fully green on the parity rig).
The e2e-config.sample.json comment is updated to reflect the new
default endpoint (http://localhost:5120 mxaccessgw) while still
pointing pre-flip rigs at the legacy OtOpcUaGalaxyHost path.
Install-Services.ps1's OtOpcUaGalaxyHost registration is intentionally
unchanged — yanking that mid-flight without a soaked parity rig would
leave any in-progress installation without a Galaxy backend at all.
PR 7.2 retires it alongside the legacy projects.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Documents the four perf surfaces shipped in Phase 6:
- Tracing surface (PR 6.1) — table of every span the driver emits +
rationale for stream-level (not per-event) coverage.
- Metrics surface (PR 6.2) — three EventPump counters, tagging
scheme, the bounded-channel design, and the
received = dispatched + dropped + in-flight invariant.
- Buffered update interval (PR 6.3) — how MxAccess.PublishingIntervalMs
flows through both subscribe paths and what's still pending on the
gw side (typed SetBufferedUpdateInterval helper).
- Soak scenario (PR 6.4) — env-var-gated 24h × 50k validation with
the CI-compressed override recipe.
- Tuned defaults (PR 6.5) — table of every default with source +
notes; rows marked "unchanged" carry the explicit "no live data
argues for changing this" caveat.
Closes with a "where to look first when something's slow" runbook
section so on-call doesn't have to re-derive the trace+metric
correlation map from primary docs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps DefaultCallTimeoutSeconds from 5 → 30. The 5s default was
provably unsafe regardless of soak data: a 50k-tag SubscribeBulk
walks the gw worker's item list serially under the MxAccess COM
apartment lock, and that scan can exceed 5s on a busy node. 30s
leaves comfortable headroom for the legitimate worst case while
still failing fast on a wedged worker.
ConnectTimeoutSeconds (10) and StreamTimeoutSeconds (0 = unlimited)
unchanged — the soak harness in PR 6.4 didn't observe pressure on
either, so they stay at their original sane values until live data
indicates otherwise.
Tuning rationale captured as a code comment in GalaxyGatewayOptions
so the next reader knows what was deliberate and what's pending live
soak data.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Long-running soak harness exercising the in-process GalaxyDriver
against a live mxaccessgw. Subscribes a configurable tag count
(default 50_000), holds the subscription for a configurable duration
(default 24h), polls the EventPump's three counters every minute, and
asserts:
- events.received continues to grow (gw stream isn't stuck)
- events.dropped stays under a configurable percent ceiling
(default 0.5%)
- process working-set doesn't grow >1 GB above baseline (leak guard)
Always skipped unless the operator opts in via OTOPCUA_SOAK_RUN=1.
Tag count, duration, and drop ceiling are env-overridable
(OTOPCUA_SOAK_TAGS / OTOPCUA_SOAK_MINUTES / OTOPCUA_SOAK_DROP_PCT) so
a smoke run can compress the scenario for CI gating.
Per-minute progress is logged as a CSV-style line to stdout so an
operator can grep the test runner output mid-run. PR 6.5 consumes the
data this scenario emits to tune MxGatewayClientOptions defaults.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires MxAccess.PublishingIntervalMs into the gw's SubscribeBulk
bufferedUpdateIntervalMs parameter on both subscribe paths:
- GalaxyDriver.SubscribeAsync — when the caller passes TimeSpan.Zero
(typical for infrastructure callers like the deploy watcher), the
driver substitutes _options.MxAccess.PublishingIntervalMs. When the
caller sets a non-zero interval (the server's UA subscription
publishingInterval), that wins.
- PerPlatformProbeWatcher — new bufferedUpdateIntervalMs ctor parameter
defaulting to 0 (gw default cadence). GalaxyDriver passes
_options.MxAccess.PublishingIntervalMs so probe ScanState changes
publish at the configured rate.
Tests: caller-wins-when-non-zero, fallback-to-config-when-zero on the
driver; default-zero, configured-forwarded, negative-rejected on the
probe watcher.
A session-level SetBufferedUpdateInterval RPC exists in the gw protocol
(MxCommandKind.SetBufferedUpdateInterval) but the .NET client doesn't
expose a typed helper yet — adjusting an existing subscription's
interval is a follow-up. Today's path subscribes once with the right
interval, which covers the common case.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Decouples the gw stream-read loop from the listener-fanout loop with a
bounded Channel<MxEvent> (default capacity 50_000) sitting between them.
When a slow listener fills the channel, the producer's TryWrite returns
false and we count the drop rather than back-pressuring the gw stream.
Three counters on the ZB.MOM.WW.OtOpcUa.Driver.Galaxy meter expose the
pressure curve before it manifests as user-visible loss:
- galaxy.events.received — MxEvents read from StreamEvents
- galaxy.events.dispatched — MxEvents that made it through to OnDataChange
- galaxy.events.dropped — MxEvents discarded because the channel was full
Each measurement carries a galaxy.client tag so multi-driver hosts can
split by source. The driver wires _options.MxAccess.ClientName into the
new EventPump constructor parameter.
Tests: drop-newest under pressure, capacity validation, and per-pump
measurement filtering (xUnit can run other pump tests in parallel and
their measurements land on the same listener — the test filters to its
own client name).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
In-box ActivitySource ("ZB.MOM.WW.OtOpcUa.Driver.Galaxy") wrapped around
the three gw-facing seams via decorators:
- TracedGalaxySubscriber — galaxy.subscribe_bulk / galaxy.unsubscribe_bulk
/ galaxy.stream_events spans. Stream span covers the entire stream
lifetime with a galaxy.event_count tag (per-event spans would dominate
the trace volume at 50k tags / 1Hz; PR 6.2 owns per-event metrics).
- TracedGalaxyDataWriter — galaxy.write spans tagged with
galaxy.tag_count, galaxy.secured_write_count (split between FreeAccess
/Operate vs Tune/Configure/VerifiedWrite, computed only when a listener
is recording so the hot path stays free), galaxy.success_count.
- TracedGalaxyHierarchySource — galaxy.get_hierarchy spans tagged with
galaxy.object_count.
GalaxyDriver.BuildProductionRuntimeAsync wraps the production seams in
the decorators. The driver itself doesn't take an OpenTelemetry package
dependency — System.Diagnostics.ActivitySource is in-box; the host
process picks the listener.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tabular scenario × result map for the seven Phase 5 parity scenarios
(BrowseAndRead, Subscribe, Write, Alarm, History, Reconnect, ScanState).
Each row records the assertion strength (green strict, yellow soft) and
flags accepted-delta cases:
- Transport-entry host name divergence (legacy = Galaxy.Host process,
mxgw = MxAccess.ClientName)
- Reconnect latency cadence — different paths, both correct for their
own session shape
- Sampled-read value drift (we pin StatusCode + type, not value)
- Event-rate ±50% tolerance over a 3s window
- Per-driver IHistoryProvider absence (architectural pin from PR 1.3)
Phase 7 (PR 7.1) consumes this matrix as the default-flip gate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes Phase 5 scenario coverage. Both
GalaxyRuntimeProbeManager (legacy) and PerPlatformProbeWatcher (PR 4.7)
must surface the same per-host status stream:
- GetHostStatuses_emits_same_host_set_after_Discover — drives Discover
on both backends, waits 1.5s for the probe watcher's first push, then
asserts the platform-host set agrees (transport-entry names differ
by design — legacy uses the Galaxy.Host process identity, mxgw uses
MxAccess.ClientName, so we strip those before comparing).
- GetHostStatuses_state_per_platform_matches_across_backends — for
every overlapping platform host, the HostState must be identical.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Reinitialize_returns_both_backends_to_Healthy — drives
ReinitializeAsync on each backend, asserts DriverState.Healthy
afterwards, then re-reads a 3-tag sample to confirm the runtime
surface is back. Recovery latency isn't pinned tightly (legacy = pipe
+ MxAccess COM client, mxgw = re-Register gw session — different
cadences are expected).
- Health_state_diverges_only_when_one_backend_is_in_recovery — soft
pin that both backends sit in Healthy or Degraded after init.
A tighter fault-injection scenario (toxiproxy-style) is the 5.7
follow-up — landed when the parity rig grows that capability.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Galaxy history reads route through the server-owned HistoryRouter
(Phase 1, PR 1.3) — neither Galaxy backend implements IHistoryProvider
directly. Parity surface here is the routing decision:
- Discover_emits_same_historized_attribute_set_for_both_backends — the
IsHistorized attribute set must agree symmetric-set-wise; that's what
HistoryRouter consumes when deciding whether to route a HistoryRead to
the Wonderware historian sidecar.
- Neither_Galaxy_backend_implements_IHistoryProvider_directly — pins
the architectural decision so a regression that re-introduces a
per-driver history path fires.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Discover_emits_same_AlarmConditionInfo_per_alarm_attribute — both
backends produce the same alarm-condition source-node-id set, with
matching SourceName / InitialSeverity / InAlarmRef / DescAttrNameRef
per condition. Skips when the rig's Galaxy carries no alarm-marked
attributes.
- Discover_marks_at_least_one_alarm_attribute_when_dev_Galaxy_has_alarms
— IsAlarm-marked variable count parity, soft-pinned (count must
match across backends but doesn't have to be non-zero).
Alarm-event persistence (the SQLite store-and-forward → Wonderware
historian event store path) is exercised in PR 5.6 against the
historian sidecar.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both backends route a write through the same path keyed off the attribute's
SecurityClassification, so a single write request must produce the same
StatusCode on each:
- FreeAccess_or_Operate_write_returns_same_StatusCode_on_both_backends
picks the first numeric FreeAccess/Operate attribute and writes 0.0.
- Configure_class_write_routes_through_secured_path_on_both_backends
picks a Configure/Tune attribute, writes through the secured path,
asserts StatusCode parity (the test doesn't care whether the write
succeeds — only that both backends produce the same outcome).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Subscribe_returns_a_handle_for_each_backend — both backends accept
the same full-reference list and return a non-null handle, with
symmetric Unsubscribe cleanup.
- Subscribe_event_rate_within_tolerance_for_a_3s_window — counts
OnDataChange invocations on each backend across a 3s window and
asserts the mxgw/legacy ratio sits in [0.5, 1.5]. Skips when the
sampled tags don't change in the window (configuration-only Galaxy).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>