The docker-dev sql service had no volume, so its data lived in the container
writable layer. A recreate silently dropped the OtOpcUa database and every host
node failed its configdb health check (AdminUI 503) until an operator re-ran
'dotnet ef database update' + the cluster-seed. Add a named volume
(otopcua-mssql-data -> /var/opt/mssql) so the migrated schema + seeded clusters
survive 'docker compose up' cycles.
docker-dev un-stubbed → binds zb-shared-glauth on 10.100.0.35:3893 (dc=zb,dc=local)
via cn=serviceaccount; sign in multi-role/password (group→role seeded by
seed-clusters.sql). Per-VM C:\publish\glauth + base DNs dc=lmxopcua/dc=otopcua
obsolete. Source of truth: scadaproj/infra/glauth/.
The 6 admin/site host containers drop DevStubMode and bind the shared dev GLAuth
(scadaproj/infra/glauth/, dc=zb,dc=local) via cn=serviceaccount. seed-clusters.sql
seeds system-wide LdapGroupRoleMapping rows OtOpcUa-Admins->Administrator,
OtOpcUa-Designers->Designer, OtOpcUa-Viewers->Viewer (bare-RDN group keys, matching
the shared lib's ToGroupShortName). Verified: multi-role -> Viewer+Designer+
Administrator at :9200 via real LDAP.
Opc.Ua.Server was pinned 1.5.374.126 while Client/Configuration were 1.5.378.106, so the
published Host unified Opc.Ua.Core to 1.5.378 (which dropped Opc.Ua.INodeIdFactory that Server
1.5.374 referenced). Every driver-role node (and the fused site nodes) failed to start the OPC
UA server with TypeLoadException, leaving the OPC data plane dead and the site UIs at 503.
Align all OPC UA packages to 1.5.378.106 (bump Server; drop the Opc.Ua.Configuration/Client
VersionOverrides in OpcUaServer + its integration tests) and port the server host to the
1.5.378 async API:
- ApplicationInstance requires an ITelemetryContext ctor (DefaultTelemetry.Create)
- Start/Stop/LoadApplicationConfiguration/Validate -> async; CheckApplicationInstanceCertificate
-> CheckApplicationInstanceCertificatesAsync
- ImpersonateEventHandler is now (ISession, ImpersonateEventArgs)
- UserNameIdentityToken.DecryptedPassword is now byte[] (UTF-8 decode)
- tests ported (byte[] passwords; async discovery/session/read client API)
Verified: full solution builds, OpcUaServer unit tests 52/52, and in docker-dev all six OPC
endpoints (4840-4845) listen and the site UIs return 302 (were 503). End-to-end OPC behaviour
(read/write/subscribe/security under 1.5.378) still needs a functional client test.
The per-role overlay (appsettings.{role}.json) was appended after WebApplicationBuilder's
default sources, so it outranked environment variables — a baked role file could not be
overridden by a deployment env var. In otopcua-dev this meant appsettings.admin.json's
Security:Ldap:DevStubMode=false beat the compose's DevStub override, so every AdminUI login
attempted a real LDAP bind against a non-existent server and failed with 'Unexpected
authentication error'.
- Program.cs: re-append AddEnvironmentVariables() + AddCommandLine(args) after the role
overlay so deployment overrides keep top precedence (overlay still beats base appsettings).
- docker-dev/docker-compose.yml: the DevStub env var targeted the stale 'Authentication:Ldap'
section; the code reads 'Security:Ldap'. Corrected the prefix on every host node (+ comment).
Dev AdminUI login now signs in as Administrator via the DevStub bypass.
CODE-REALITY:
- Line 8: "Fifth (final)" → "Fifth of six"; TwinCAT is not the final CLI —
FOCAS (sixth) follows it. The "final" label was stale ordinal drift
from when there were fewer CLIs; 6 projects confirmed in
src/Drivers/Cli/.
- probe per-command flag table: `--type` row was missing the `-t`
shorthand. ProbeCommand.cs:25 declares
[CommandOption("type", 't', ...)] — same '-t' shorthand used by read,
write, subscribe; the probe table was the only one that omitted it.
Fixed to `-t` / `--type` for consistency.
STRUCTURAL: no rows in links-report.md for this doc.
STALE-STATUS: no state words found.
INLINE COMPLETENESS: no inventory-diff gaps for this doc.
CODE-REALITY:
- Line 7: "Fourth of four" → "Fourth of six"; there are 6 driver CLIs
(Modbus, AbCip, AbLegacy, S7, TwinCAT, FOCAS); confirmed by
src/Drivers/Cli/ project count.
- read section: removed the `DB10.STRING[0] -t String --string-length 80`
example that documented an unusable code path. String (and Int64,
UInt64, Float64, DateTime) live in S7DataType but are blocked in
S7Driver.UnimplementedDataTypes; any attempt returns BadNotSupported
(src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs:327-333 and
:450). Added an explicit "not yet implemented" note with the
source location so readers know why those types are omitted.
STRUCTURAL: no rows in links-report.md for this doc.
STALE-STATUS: no state words found.
INLINE COMPLETENESS: no inventory-diff gaps for this doc.
CODE-REALITY: all four verbs (probe/read/write/subscribe), all common flags
(-g/--gateway, -P/--plc-type, --timeout-ms, --verbose), AssemblyName
otopcua-ablegacy-cli — all match code exactly. PCCC type table (Bit/Int/Long/
Float/AnalogInt/String/TimerElement/CounterElement/ControlElement) confirmed
against AbLegacy ReadCommand.cs:25 and WriteCommand.cs:24.
INLINE COMPLETENESS: corrected CLI roster count from "third of four" to
"third of six" to match DriverClis.md (S7, TwinCAT, FOCAS are also shipped).
Evidence: docs/Driver.AbLegacy.Cli.md:7 vs docs/DriverClis.md roster table.
STRUCTURAL: ../tests/.../Docker/README.md link confirmed present on disk.
check_links.py: 0 rows for this file.
CODE-REALITY fixes (file:line evidence):
- Read/Write tab write description was wrong: claimed the service reads
current value first to determine the target type before writing.
ReadWriteViewModel.WriteAsync (ReadWriteViewModel.cs:97-113) calls
WriteValueAsync directly with the raw string — no pre-read.
The type-inferring read-before-write lives only in the Subscriptions
tab write dialog (SubscriptionsViewModel.ValidateAndWriteAsync).
Button label is also "Write", not "Send" (ReadWriteView.axaml:35).
- Settings save timing was incomplete: MainWindowViewModel.DisconnectAsync
(MainWindowViewModel.cs:309) calls SaveSettings() on disconnect too;
doc said only "after successful connect and on window close".
STRUCTURAL: no rows in links-report.md for this file.
STALE-STATUS: no stale-status language found.
INLINE COMPLETENESS: no inventory gaps found.
CODE-REALITY: all four verbs (probe/read/write/subscribe), all common flags
(-h/--host, -p/--port, -U/--unit-id, --timeout-ms, --disable-reconnect,
--verbose), AssemblyName otopcua-modbus-cli — all match code exactly.
INLINE COMPLETENESS: corrected CLI roster count from "four" to "six" to
match DriverClis.md which lists all six shipped CLIs (Modbus, AB CIP,
AB Legacy, S7, TwinCAT, FOCAS); also added FOCAS to the explicit list.
Evidence: docs/Driver.Modbus.Cli.md:8 vs docs/DriverClis.md roster table.
STRUCTURAL: no link rows for this doc in links-report.md; v2/modbus-addressing.md
target confirmed present. check_links.py: 0 rows for this file.
CODE-REALITY: verified all 8 verbs + flags against src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/.
Verb set confirmed exact (connect/browse/read/write/subscribe/historyread/alarms/redundancy);
common options (-u/-U/-P/-S/-F/--verbose) match CommandBase.cs:32-64; per-command flags all match.
Fixes:
- Aggregate map: StandardDeviation maps to AggregateFunction_StandardDeviationPopulation,
not ...Sample (AggregateTypeMapper.cs:26). Doc table corrected.
- STALE: test count 52 -> 77 (77 [Fact] across tests/Client/...CLI.Tests, no Theory).
STRUCTURAL: links-report.md had no rows for docs/Client.CLI.md; check_links.py clean.
Executable name otopcua-cli is the CliFx SetExecutableName (Program.cs:12); csproj has no
AssemblyName, so dotnet-run invocation in CLAUDE.md is correct — no change.
STRUCTURAL (links-report.md):
- Repointed missing src/.../Security/Ldap/LdapAuthService.cs -> the real
OtOpcUaLdapAuthService.cs (Ldap/OtOpcUaLdapAuthService.cs implements
ILdapAuthService). Class was reorganized as a wrapper over shared
ZB.MOM.WW.Auth.Ldap. check_links now clean for docs/security.md.
CODE-REALITY — transport profiles (OpcUaApplicationHost.cs:15-23,59-64,374-409):
- Only THREE profiles exist: None, Basic256Sha256Sign,
Basic256Sha256SignAndEncrypt (NO hyphens, NO underscores). Removed the four
fabricated Aes128/Aes256 rows. Config binds by enum-member name; hyphenated
form does NOT bind. Documented this + the empty-list fallback to None.
- Config section is OpcUa (not OpcUaServer); key is the LIST
EnabledSecurityProfiles (not singular SecurityProfile). Program.cs:120 binds
'OpcUa'; Certificates.razor:80 reads OpcUa:PkiStoreRoot.
- No SecurityProfileResolver class exists — stated so explicitly.
CODE-REALITY — LDAP (LdapOptions.cs:21, OtOpcUaLdapAuthService.cs):
- Section is Security:Ldap (LdapOptions.SectionName), not OpcUaServer:Ldap.
- Authenticator is OtOpcUaLdapAuthService (wrapper) + LdapOpcUaUserAuthenticator
(IOpcUaUserAuthenticator.AuthenticateUserNameAsync), not bespoke
LdapUserAuthenticator/IUserAuthenticator.
- UseTls bool -> Transport enum (Ldaps/StartTls/None); AllowInsecureLdap ->
AllowInsecure. Added Enabled master switch + DevStubMode.
- Group->role mapping is downstream via IGroupRoleMapper<string>
(OtOpcUaGroupRoleMapper), NOT in the auth service. ILdapGroupsBearer and
DenyAllUserAuthenticator do not exist (fallback is NullOpcUaUserAuthenticator).
- GroupToRole values corrected to canonical roles (Viewer/Designer/
Administrator/Operator).
CODE-REALITY — ACL trie (TriePermissionEvaluator.cs, PermissionTrieCache.cs,
NodeScope.cs, NodePermissions.cs):
- NodePermissions backing type is int (not uint); lives in Configuration/Enums.
- Authorize(UserAuthorizationState, OpcUaOperation, NodeScope) returns
AuthorizationDecision.
- Evaluator is strictly fail-CLOSED. Removed the fabricated
'fail-open-during-transition' + Authorization:StrictMode key (no StrictMode
anywhere in source).
- Cache: generation-sealed Install/Invalidate/Prune. AclChangeNotifier does
NOT exist — removed.
- Added the SystemPlatform (Galaxy) scope hierarchy variant.
CODE-REALITY — control plane (AdminRole.cs, ServiceCollectionExtensions.cs:
113-131):
- AdminRole members are Viewer/Designer/Administrator (Task 1.7 rename from
ConfigViewer/ConfigEditor/FleetAdmin). DriverOperator/FleetAdmin are POLICY
names; DriverOperator requires roles Operator|Administrator.
CODE-REALITY — analyzer (UnwrappedCapabilityCallAnalyzer.cs:99-103,
AnalyzerReleases.Shipped.md):
- Confirmed category OtOpcUa.Resilience + severity Warning (already correct).
Corrected 'Five tests' (suite has 26 cases) and AlarmSurfaceInvoker
wrapper-home wording.
OTHER FIXES:
- v2 header: removed false AddJwtBearer/IPostConfigureOptions<JwtBearerOptions>
claim — auth is Cookie-only; JWT is mint-only via /auth/token for external
consumers (JwtTokenService.cs:25-48).
- Certificates.razor is a read-only viewer; removed fabricated
CertTrustService/CertTrustOptions promote claim.
- Audit: writer is AuditWriterActor (not AuditLogService); softened the
unverifiable server-side 'AUDIT:' Serilog-prefix claim.
STALE-STATUS / CODE-REALITY fixes:
- Table row ReleasedAt/ReleasedBy: "FleetAdmin" → "Administrator" (AdminRole
enum renamed in CanonicalizeAdminRoles migration). ReleasedBy now documents
that it is the LDAP operator name passed as explicit @ReleasedBy param — not
SUSER_SNAME() — per migration 20260522000001_AddReleasedByToReleaseExternalIdReservation.
- §4 Release: "FleetAdmin" → "Administrator"; added @ReleasedBy required param
requirement matching the updated stored-proc signature; replaced "SUSER_SNAME()"
attribution claim with the correct explicit-param description.
- §The Admin page: replaced entirely. Actual Reservations.razor uses bare
[Authorize] (not [Authorize(Policy="FleetAdmin")] and not "CanPublish").
The page is a read-only flat list (no Active/Released split, no Release row
action, no Release dialog). Redirected release-flow readers to
docs/v2/admin-ui.md §"Release an external-ID reservation".
Evidence:
src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Reservations.razor:2
src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/AdminRole.cs:36
src/Server/ZB.MOM.WW.OtOpcUa.Security/ServiceCollectionExtensions.cs:130
src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260522000001_AddReleasedByToReleaseExternalIdReservation.cs
Structural (broken paths):
- Line 73: ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client/Contracts/
→ ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Contracts/
(contracts extracted to their own top-level project; no Contracts/ subfolder)
- Line 73: ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/Pipe/
→ ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/Ipc/
(directory renamed from Pipe/ to Ipc/)
Verified: both new targets exist on disk.
Code-reality (bitness):
- Line 10: historian sidecar platform "x86 (32-bit)" → "x64 (64-bit)"
Evidence: ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/
ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.csproj
<PlatformTarget>x64</PlatformTarget> with explicit comment:
"x64 — AVEVA Historian 2020 ships an x64 build of aahClientManaged …
The earlier x86 default was inherited from v1's Galaxy.Host bitness
(MXAccess COM, retired in PR 7.2) and didn't reflect any constraint
of the Historian SDK itself."
Stale-status:
- Line 69: removed "Task 63 traefik docs — TODO"; link retargeted to
existing docs/v2/Architecture-v2.md (Traefik section present at line 114)
- Line 77: removed "v2 rewrite tracked as plan Task 62" — install script
ships complete at scripts/install/Install-Services.ps1
STALE-STATUS (OpcPlcFixture.cs:39):
- "What the fixture is": opc.tcp://localhost:50000 → opc.tcp://10.100.0.35:50000
(shared Docker host migrated 2026-04-28; fixture already defaults to 10.100.0.35)
CODE-REALITY (OpcUaClientSmokeTests.cs — 3 integration tests open real Secure Channels):
- "What it does NOT cover" §1 ("No UA Secure Channel is ever opened") was wrong
for the integration suite which does open real channels. Rewritten to scope the
no-Secure-Channel claim to the unit suite and list what the integration suite
still doesn't exercise (non-anonymous security policies, signing/encryption,
chunk assembly, keep-alive).
- "When to trust" table: added Integration (opc-plc) column; noted that real OPC UA
read + subscribe ARE covered by integration tests; write not yet exercised on wire.
NOTE on IRediscoverable: OpcUaClientDriver does NOT implement IRediscoverable
(verified: no reference in src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/).
Doc makes no such claim — no change needed for that aspect.
INLINE COMPLETENESS:
- "Key fixture / config files": added OpcPlcFixture.cs, OpcUaClientSmokeTests.cs,
and Docker/docker-compose.yml entries with correct endpoints and flags.
- Added explicit note in OpcUaClientDriver.cs entry: implements IAlarmSource +
IHistoryProvider (unique among drivers); does NOT implement IRediscoverable.
STRUCTURAL: no rows in links-report.md for this doc.
VERIFY: check_links.py — 0 rows for OpcUaClient-Test-Fixture.md.
STALE-STATUS (Snap7ServerFixture.cs:40):
- TL;DR + "What the fixture is": localhost:1102 → 10.100.0.35:1102 (shared
Docker host migrated 2026-04-28; fixture already defaults to 10.100.0.35)
CODE-REALITY (S7_1500SmokeTests.cs exists and sends real S7comm):
- "What it does NOT cover" §1 ("No ISO-on-TCP frame is ever sent") was
simply wrong — the integration suite DOES send real S7comm. Rewritten
to clarify that the unit suite uses IS7Client fakes while the integration
suite exercises the full wire path.
- "What it does NOT cover" §2 ("successful read not tested end-to-end")
was also wrong — Driver_reads_seeded_u16_through_real_S7comm does exactly
that. Rewritten to scope the error-branch-only claim to unit tests.
- "When to trust" table: added Integration (python-snap7) column reflecting
what the existing S7_1500SmokeTests actually answer.
- "Follow-up candidates" §1: removed the suggestion to build a Snap7 server
fixture — python-snap7 fixture (task #216) already ships. Follow-ups now
correctly list Plcsim Advanced and real lab rig only.
INLINE COMPLETENESS:
- "Key fixture / config files": was missing all integration test artefacts.
Added Snap7ServerFixture.cs, S7_1500SmokeTests.cs, Docker/docker-compose.yml,
and Docker/profiles/s7_1500.json with descriptions matching file contents.
STRUCTURAL: no broken links in links-report.md for this doc.
VERIFY: check_links.py — 0 rows for S7-Test-Fixture.md.
STALE-STATUS: TL;DR claimed "Wire-level round-trip against ab_server PCCC
mode currently fails with BadCommunicationError on read/write (verified
2026-04-20)." Docker/README.md §Known limitations explicitly states the
root cause was ab_server's empty-CIP-path gate, not a pccc.c gap, and that
N/F/L files round-trip cleanly with the /1,0 path. AbLegacyReadSmokeTests.cs
confirms tests pass against the fixture. Rewrote TL;DR + What-the-fixture-is
opening to reflect current passing state; residual gap is only B3 bit-file
writes (0x803D0000).
STALE-STATUS: Lifecycle probe listed as localhost:44818.
AbLegacyServerFixture.cs:57,119 default is 10.100.0.35:44818 (shared Docker
host, migrated 2026-04-28). Fixed.
INLINE COMPLETENESS: Follow-up item 1 phrased as future work ("smoke suite
passes today for N/F/L…"); tightened to describe the current passing state
and narrowed the remaining action to the bit-file write gap.
Verified: python3 .docs-audit/check_links.py — zero rows for this doc.
STRUCTURAL: links-report.md row — path MISSING src/tools/ab_server/.
ab_server is not in this repo; it lives in the upstream libplctag/libplctag
GitHub repo and is cloned + built inside Docker/Dockerfile. Rewrote Binary
bullet to describe it as an external upstream source (no local path reference
that fails the link checker).
STALE-STATUS: Lifecycle TCP-probe host was listed as 127.0.0.1:44818
(AbServer-Test-Fixture.md:21). AbServerFixture.cs:35,72 default is
10.100.0.35:44818 (shared Docker host, migrated 2026-04-28). Fixed.
CODE-REALITY: Micro800 profile Notes quoted "ab_server has no --plc micro800
— falls back to controllogix emulation." Incorrect: Docker/docker-compose.yml
micro800 service uses --plc=Micro800; AbServerProfile.cs:49 confirms
"--plc=Micro800 mode (unconnected-only, empty path)." Updated Notes quote
and summary table row to match actual compose behaviour.
Verified: python3 .docs-audit/check_links.py — zero rows for this doc.
STALE-STATUS: TL;DR + Lifecycle section referred to "localhost" as the
simulator address (Modbus-Test-Fixture.md:7,19). Fixture default is
10.100.0.35:5020 (shared Docker host, migrated 2026-04-28) confirmed by
ModbusSimulatorFixture.cs:36. Updated both prose occurrences.
INLINE COMPLETENESS: Follow-up item 1 claimed MODBUS_SIM_ENDPOINT
lacked documentation; the env var is already documented in this page +
CLAUDE.md. Reworded to reflect actual gap (cross-reference to
test-data-sources.md only).
Verified: python3 .docs-audit/check_links.py — zero rows for this doc.
CODE-REALITY (matrix corrected against driver class declarations):
- Galaxy: GalaxyDriver.cs:38-39 implements IDriver, ITagDiscovery,
IReadable, IWritable, ISubscribable, IRediscoverable,
IHostConnectivityProbe, IAlarmSource. Removed the bogus
IHistoryProvider (no IHistoryProvider refs anywhere in the Galaxy
project); added the missing IRediscoverable. Replaced the stale
out-of-process Host/Proxy/named-pipe quirk + the dead
`Driver.Galaxy.{Shared,Host,Proxy}` path: per CLAUDE.md PR 7.2 those
retired; the real driver is in-process .NET 10 over gRPC to the
external mxaccessgw gateway (GalaxyDriver.cs:20-21 doc comment).
Project path corrected to Driver.Galaxy (+ .Browser, .Contracts).
- Modbus: ModbusDriver.cs:21-22 — added missing IPerCallHostResolver.
- FOCAS: FocasDriver.cs:20-21 — added missing IWritable (it IS
implemented; WriteAsync returns BadNotWritable for every point,
FocasDriver.cs:317).
- S7 (S7Driver.cs:31-32), AbCip (AbCipDriver.cs:27-28),
AbLegacy (AbLegacyDriver.cs:13-14, no IAlarmSource confirmed),
TwinCAT (TwinCATDriver.cs:13-14), OpcUaClient
(OpcUaClientDriver.cs:31) verified — already correct.
- Added the 9th family Historian.Wonderware as a server-side historian
sink (HistorianDataSource.cs:19 `: IHistorianDataSource`), and added
IHistorianDataSource to the capability-interface list.
- Clarified OpcUaClient as the only driver-side IHistoryProvider; fixed
the HistoricalDataAccess cross-ref accordingly (the Aveva Historian
path is the Wonderware IHistorianDataSource sink, not a Galaxy
IHistoryProvider).
- Added an alarm-source roster to the AlarmTracking cross-ref.
STRUCTURAL (4 dead links repointed to the docs/v1 archive, all verified
to exist):
- ../HistoricalDataAccess.md -> ../v1/HistoricalDataAccess.md (x2)
- ../Subscriptions.md -> ../v1/Subscriptions.md
- Galaxy-Repository.md -> ../v1/drivers/Galaxy-Repository.md
- Galaxy-Test-Fixture.md -> ../v1/drivers/Galaxy-Test-Fixture.md
check_links.py now reports zero rows for docs/drivers/README.md.
STALE-STATUS: removed out-of-process/named-pipe Galaxy wording; noted
native MxAccess alarms work end-to-end; dropped the FOCAS "Tier-C
two-project deployment" phrasing from the per-driver section.
All four dimensions verified against source:
STRUCTURAL: no rows in links-report.md; all 4 linked docs resolve:
docs/v2/driver-specs.md, docs/v2/focas-version-matrix.md,
docs/v2/implementation/focas-wire-protocol.md,
docs/drivers/FOCAS-Test-Fixture.md.
STALE-STATUS: no date anchors, "blocked", "pending", "not yet", "will"
or TODO phrases found.
CODE-REALITY (verified against src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/):
- IAlarmSource implemented at FocasDriver.cs:21
- IWritable correctly returns BadNotWritable (read-only design)
- All capability interfaces (IReadable, ITagDiscovery, ISubscribable,
IHostConnectivityProbe, IPerCallHostResolver) match FocasDriver.cs:21
- Wire/ directory: WireFocasClient.cs present
- FocasCapabilityMatrix.cs present
INLINE COMPLETENESS: inventory-diff.md records FOCAS as the only
fully-covered driver (overview + CLI + fixture) — no gaps to add.
STRUCTURAL
- docs/drivers/FOCAS-Test-Fixture.md line 140: replaced stale
`Series/FixedTreePopulatesTests.cs` reference (file deleted) with
`Series/WireBackendTests.cs` — the current home of all fixed-tree
end-to-end integration tests (verified: ls Series/ shows only
WireBackendTests.cs + WireBackendCoverageTests.cs).
STALE-STATUS
- Removed `**Status:** as of 2026-04-24` header (date-anchored, stale).
The architecture description that followed was accurate; the date anchor
served no purpose once the shim era is closed.
CODE-REALITY
- Line 55: TCP-probe skip gate now mentions `OTOPCUA_FOCAS_SIM_ENDPOINT`
override (verified in FocasSimFixture.cs line 22 / 49).
FOCAS.md: no changes — all claims verified accurate against source.
- IAlarmSource implemented: FocasDriver.cs:21
- IWritable returns BadNotWritable: FocasDriver.cs (IWritable body)
- All capability interfaces listed in capability table confirmed in
FocasDriver.cs:21 class declaration
- All linked files exist and resolve correctly
CODE-REALITY (known defect): the capability-surface declaration line
omitted IAlarmSource and IAsyncDisposable. GalaxyDriver.cs:39 actually
declares: IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable,
IRediscoverable, IHostConnectivityProbe, IAlarmSource, IDisposable,
IAsyncDisposable. Doc line corrected to match exactly, and an IAlarmSource
row added to the capability table (Runtime/GatewayGalaxyAlarmFeed.cs +
Runtime/GatewayGalaxyAlarmAcknowledger.cs).
STALE-STATUS: the v1-doc move note claimed Galaxy-Repository.md /
Galaxy-Test-Fixture.md 'are being moved to docs/v1/ by a parallel cleanup
track' — that move is complete; they live at docs/v1/drivers/. Rewrote to
present tense and linked the real targets.
Verified against source: deploy-watch is a gRPC stream
(GatewayGalaxyDeployWatchSource forwards WatchDeployEventsAsync via
GalaxyRepositoryClient, not a direct DB poll); contained-name<->tag-name
translation (GalaxyDiscoverer.cs:49,60); DataTypeMap at Browse/DataTypeMap.cs;
IGalaxyHierarchySource / IGalaxyDeployWatchSource / DeployWatcher all present.
check_links.py: zero rows for docs/drivers/Galaxy.md.
CODE-REALITY (file:line evidence)
- Definition section: removed reference to non-existent
Phase7EngineComposer.ProjectScriptedAlarms; Phase7Composer is a pure
data composer (entities → Phase7CompositionResult)
(src/Server/.../OpcUaServer/Phase7Composer.cs:82-183)
- AlarmSeverity: removed "Phase7EngineComposer.MapSeverity bands it" —
no such class exists; clarified that AlarmSeverity is defined in
Core.Abstractions/IAlarmSource.cs not in AlarmTypes.cs
(src/Core/.../Core.Abstractions/IAlarmSource.cs:87)
- State persistence: replaced "Stream E wires..." planning language with
actual production class EfAlarmActorStateStore
(src/Server/.../Runtime/ScriptedAlarms/EfAlarmActorStateStore.cs)
- Composition section: replaced Phase7EngineComposer / Phase7ComposedSources
references (non-existent) with the actual v2 actor-system composition
path (ScriptedAlarmEngine + ScriptedAlarmActor + driver-role host startup)
- Key source files: AlarmTypes.cs annotation corrected (adds ShelvingKind,
names all four state enums, notes AlarmSeverity lives in Core.Abstractions)
- Key source files: Phase7Composer.cs annotation corrected to "pure data
composer"
- Key source files: ScriptedAlarmActor.cs annotation corrected to describe
AlarmTransitionEvent + DPS alerts topic (not "OPC UA variable reads")
- Key source files: added EfAlarmActorStateStore as the production
IAlarmActorStateStore implementation
STALE-STATUS
- "Stream E wires the production implementation" — removed; production
implementation ships and is named EfAlarmActorStateStore
STRUCTURAL: Fix broken docs/HistoricalDataAccess.md link → docs/v1/HistoricalDataAccess.md
(file moved to v1/ archive; confirmed present at docs/v1/HistoricalDataAccess.md).
CODE-REALITY: Opening paragraph incorrectly attributed OnReadValue/OnWriteValue hook wiring
to GenericDriverNodeManager. Verified: GenericDriverNodeManager is a plain IDisposable
address-space population helper, not a CustomNodeManager2; it has no read/write hooks
(src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs). The v1 DriverNodeManager
that wired those hooks was deleted at 76310b8 (2026-05-26 "chore(cleanup): delete
OtOpcUa.Server, OtOpcUa.Admin, and obsolete v1 tests"). The ADR-002 Phase 7 Stream G
DriverNodeManager replacement is planned but not yet implemented. Current v2 architecture
is a push model: OtOpcUaNodeManager (src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/
OtOpcUaNodeManager.cs) is the CustomNodeManager2; reads return the value last pushed via
WriteValue() from the Akka actor layer. Opening paragraph and Key source files section
updated to reflect present truth; behavioral-contract sections preserved for the ADR-002
planned DriverNodeManager. Flagged in .docs-audit/code-bug-flags.md as CBF-ReadWrite.
STRUCTURAL
- Repoint Subscriptions.md link → v1/Subscriptions.md (doc line 104;
target confirmed at docs/v1/Subscriptions.md)
CODE-REALITY (file:line evidence)
- Intro: replace non-existent NodeScopeResolver / DriverNodeManager with
actual EquipmentNodeWalker + GenericDriverNodeManager; NodeSourceKind
is stamped by EquipmentNodeWalker.Walk at address-space build time
(src/Core/.../OpcUa/EquipmentNodeWalker.cs:231,256)
- ScriptSandbox.Build: doc claimed allow-list was "System.Private.CoreLib"
by name; actual code enumerates TRUSTED_PLATFORM_ASSEMBLIES filtered to
System.* + netstandard + mscorlib + Microsoft.Win32.Registry
(src/Core/.../ScriptSandbox.cs:97-127)
- Compile pipeline: doc said "three-step gate"; code has 5 steps —
EnforceSingleRunMember injection guard (Core.Scripting-013) was missing,
and PE emit is a distinct step before ALC load
(src/Core/.../ScriptEvaluator.cs:80-171)
- ForbiddenTypeAnalyzer: doc listed System.Threading.Thread in
ForbiddenNamespacePrefixes; code explicitly does NOT put it there
("Thread's containing namespace is System.Threading, so a prefix check
never matches") and instead denies it via ForbiddenFullTypeNames; also
added System.Runtime.Loader and System.Threading.ThreadPool/Timer to
match the actual deny-list (Core.Scripting-010/-012)
(src/Core/.../ForbiddenTypeAnalyzer.cs:60-139)
- Dispatch section: DriverNodeManager → GenericDriverNodeManager;
NodeScopeResolver.IsWriteAllowedBySource (non-existent) removed
- Upstream reads: removed non-existent CachedTagUpstreamSource / Phase7EngineComposer
references; describe actual DependencyMuxActor → VirtualTagActor feed
- Composition: replaced entire section; Phase7EngineComposer /
Phase7ComposedSources / PrepareAsync / DriverSubscriptionBridge /
CachedTagUpstreamSource do not exist in the codebase; Phase7Composer is
a pure data composer (entities → Phase7CompositionResult)
(src/Server/.../OpcUaServer/Phase7Composer.cs:82-183)
- Key source files: ScriptEvaluator description updated to "five-step";
Phase7Composer description corrected; runtime actor descriptions updated
STALE-STATUS
- "Definition reload: handler is not yet wired" — removed (v2 is feature-
complete; actor-based composition does not use VirtualTagEngine.Load
as a reload entry point)
STRUCTURAL: links-report.md has no rows for this doc; check_links.py clean.
STALE-STATUS / CODE-REALITY fixes (file:line evidence):
- 'Galaxy Proxy' / GalaxyProxyDriver.DiscoverAsync retired (PR 7.2) -> GalaxyDriver.DiscoverAsync delegates to GalaxyDiscoverer (Browse/GalaxyDiscoverer.cs:42); removed bogus 'AlarmExtension primitive' + 'two-pass primitive-grouping' claims (IsAlarm comes straight from the gateway hierarchy, GalaxyDiscoverer.cs:71).
- DriverNodeManager.CreateAddressSpace / DriverNodeManager.MapDataType: no such class. Root folder is created by OtOpcUaNodeManager.CreateAddressSpace (OtOpcUaNodeManager.cs:225) as a single shared 'OtOpcUa' root, EventNotifier=None (cs:234-237), not per-driver ns;s={DriverInstanceId}/urn:OtOpcUa:{id}/SubscribeToEvents|HistoryRead. Data-type resolution is OtOpcUaNodeManager.ResolveBuiltInDataType (cs:177) plus per-driver maps (Galaxy Browse/DataTypeMap.Map).
- _securityByFullRef is a Galaxy-driver-internal cache (GalaxyDriver.cs:65/682), not a node-manager field; WriteAuthzPolicy and _writeIdempotentByFullRef do not exist. Rewrote SecurityClass row to the real NodePermissions/TriePermissionEvaluator authz path (TriePermissionEvaluator.cs:78) and WriteIdempotent row to the Polly-retry semantics from DriverAttributeInfo.cs:28-35.
- NodeId scheme table rewritten: string NodeIds under one shared namespace from Config-DB ids / driver refs (Phase7Applier.cs:119-167), not ns;s={DriverInstanceId}.
- Rediscovery: OPC UA Client does NOT implement IRediscoverable (OpcUaClientDriver.cs:31); only Galaxy (DeployWatcher time_of_last_deploy) and TwinCAT (symbol-version-changed 1809) do.
- AB CIP: folder-per-device (AbCipDriver.cs:912-950), not 'per program'; UDT members fan into sub-folders, controller browse into Discovered/.
INLINE COMPLETENESS: added Source (NodeSourceKind) row; documented the two-layer builder->actor->SDK-sink architecture; added EquipmentNodeWalker.cs + Phase7Applier.cs to Key source files.
Verified DataTypeMap.cs lives at the CLAUDE.md-cited path (Driver.Galaxy/Browse/DataTypeMap.cs); contained-name/tag-name + ValueRank/ArrayDim claims cross-checked against Browse/GalaxyDiscoverer.cs:49-71.
STRUCTURAL: no broken links/paths for this doc (links-report had zero rows);
check_links.py confirms zero rows. All cited src paths verified on disk.
STALE-STATUS (v1->v2):
- Removed v1 'two separate Server/Admin processes' framing; documented the
single role-gated Host binary + OTOPCUA_ROLES gate
(src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs; AkkaClusterOptions.cs).
- Server class is OtOpcUaSdkServer (not 'OtOpcUaServer'); it wires ONE
OtOpcUaNodeManager via CreateMasterNodeManager, not one DriverNodeManager
per driver. OtOpcUaSdkServer.cs:12-26.
- Removed nonexistent OnServerStarted / LoadServerProperties overrides and
the 'DriverNodeManagers' member (no such member; grep found none).
CODE-REALITY (doc corrected to match source; no code changed):
- Class name: OtOpcUaSdkServer : StandardServer — OtOpcUaSdkServer.cs:12.
- Address space: OtOpcUaNodeManager : CustomNodeManager2, namespace
'https://zb.com/otopcua/ns', single 'OtOpcUa' root folder; push-driven via
IOpcUaAddressSpaceSink — OtOpcUaNodeManager.cs:25,27,225-251.
- Impersonation lives in OpcUaApplicationHost (not the SDK server). Uses
IOpcUaUserAuthenticator, attaches a UserIdentity (NOT RoleBasedIdentity/
IRoleBearer — neither exists), Anonymous+X509 fall through to SDK default,
failures -> BadIdentityTokenRejected (not BadIdentityTokenInvalid).
OpcUaApplicationHost.cs:159-288.
- Certificate stores default to PkiStoreRoot='pki' (relative to cwd), NOT
%LOCALAPPDATA%. Substores own/issuer/trusted/rejected.
AutoAcceptUntrustedClientCertificates default=false (doc had
Security.AutoAcceptClientCertificates default=true; key does not exist).
Removed RejectSHA1Certificates claim (not present).
OpcUaApplicationHost.cs:51,71,298-355.
- Security profiles: EnabledSecurityProfiles default = all three baseline
profiles, one endpoint per profile; not 'resolved from ServerInstance.Security
JSON, default None'. Endpoint path is .../OtOpcUa. OpcUaApplicationHost.cs:59-64,321.
- Dispatch: CapabilityInvoker is one per (DriverInstance, IDriver); pipeline
keyed (DriverInstanceId, hostName, DriverCapability). Enum member is
'Discover' (not 'Discovery'). Alarm surfaces route via AlarmSurfaceInvoker
(SubscribeAlarmsAsync/UnsubscribeAlarmsAsync/AcknowledgeAsync), per-host
fan-out. CapabilityInvoker.cs:7-19,61-156; AlarmSurfaceInvoker.cs:5-51;
DriverCapability.cs:20-41. OTOPCUA0001 analyzer is category OtOpcUa.Resilience,
severity Warning — UnwrappedCapabilityCallAnalyzer.cs:67; AnalyzerReleases.Shipped.md:10.
- Authorization: removed nonexistent AuthorizationGate / NodeScopeResolver /
Authorization:StrictMode / lax-strict mode / WriteAuthzPolicy. Documented the
real permission-trie infra under Core/Authorization/ (PermissionTrie,
TriePermissionEvaluator, NodeScope, UserAuthorizationState, AuthorizationDecision).
- Config DB: optimistic concurrency is RowVersion (per-entity), not a
'DraftRevisionToken' (no such field). sp_PublishGeneration +
sp_ComputeGenerationDiff verified in Configuration migrations.
- Redundancy: ServiceLevel republished via SdkServiceLevelPublisher
(IServiceLevelPublisher); ServiceLevelCalculator 0-255. Dropped invented
'RedundantServerArray' node; standard props are RedundancySupport +
ServerUriArray. SdkServiceLevelPublisher.cs:9-58; ServiceLevelCalculator.cs:13-23.
INLINE COMPLETENESS: documented EnabledSecurityProfiles binding key in the
Transport section (inventory-diff G3 row owner).
ORPHAN DECISION: Keep as live doc (path: keep-and-fix).
Rationale: the file carries unique v2 current content describing
the alarms-over-gateway epic architecture; docs/ScriptedAlarms.md
cross-references it explicitly. The orphan symptom is that
docs/README.md still indexes docs/v1/AlarmTracking.md — wiring
this top-level file into README.md is a follow-up task.
STRUCTURAL (dimension 2):
- docs/AlarmTracking.md line 138: Security.md → security.md (CASE-MISMATCH
from links-report.md rows 1–2). Verified: docs/security.md exists
(inode 77517627); docs/Security.md is the same file on APFS
case-insensitive FS, but the checker requires exact on-disk casing.
check_links.py: zero rows for docs/AlarmTracking.md after fix.
CODE-REALITY (dimension 4):
- line 16 table: `Phase7EngineComposer` / `Phase7EngineComposer.RouteToHistorianAsync`
→ no such class exists. Real class is `Phase7Composer`
(src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Composer.cs).
Scripted-alarm historian routing goes through ScriptedAlarmActor →
HistorianAdapterActor → IAlarmHistorianSink, not a RouteToHistorianAsync
method. Fixed to: Phase7Composer / ScriptedAlarmActor transitions →
HistorianAdapterActor → IAlarmHistorianSink.
- lines 107–123 "Historian write-back" section: referenced
`Phase7Composer.ResolveHistorianSink` (method doesn't exist in
current Phase7Composer.cs), `GalaxyProxyDriver` / `GalaxyHistorianWriter`
(retired in PR 7.2 — no such class in codebase), and `aahClientManaged`
as a direct call (now mediated through WonderwareHistorianClient).
Current architecture: NullAlarmHistorianSink default registered in
ServiceCollectionExtensions.AddOtOpcUaRuntime(); production override
is SqliteStoreAndForwardSink wrapping WonderwareHistorianClient; bridge
is HistorianAdapterActor (src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Historian/
HistorianAdapterActor.cs). Section rewritten to match code reality.
- line 108: "Program.cs" as NullAlarmHistorianSink registration site →
actual site is ServiceCollectionExtensions.cs, not Program.cs.
STALE-STATUS (dimension 3): no blocked/pending/not-yet banners found
in the top-level file; it was already written as current-state fact.
Galaxy native alarms work end-to-end (verified 2026-05-31) and the
doc correctly describes that as delivered.
CODE-BUG-FLAGS: none. All stale references were doc-side errors; the
production code is correct.
UNVERIFIABLE CLAIMS: AlarmConditionService, DriverNodeManager, ConditionSink,
DriverAlarmSourceAcknowledger, DriverWritableAcknowledger — these are
mentioned by name in the doc but their .cs files were not found in the
search. They may live under a path not searched, or may be internal
implementation details within existing files. These claims are plausible
given the architecture and were not changed.