Central cluster (2 fused admin+driver nodes) hosts the only UI + deploy
singleton; site clusters (2 driver-only nodes each) join the central mesh
and are logically separated by ClusterId. Each node applies only its own
cluster's drivers + address space on a global deploy. Approved design;
next step is the implementation plan.
A thin gateway over the admin-operations cluster singleton so CI/scripts can trigger a
deployment without the Blazor button. Forwards to the same IAdminOperationsClient.
StartDeploymentAsync; mounted on admin-role nodes. Auth is a fixed-time X-Api-Key check
against Security:DeployApiKey (orthogonal to the cookie-only web auth); AllowAnonymous so the
auth fallback doesn't 401 it, self-disabling (503) until the key is set. Outcome->status:
202/200/409/422. Unit tests for the key check + outcome mapping; HTTP E2E (real auth + real
deploy via the 2-node harness). Documented in docs/security.md.
Entities -> Phase7Composer.Compose -> MaterialiseHierarchy + MaterialiseEquipmentTags ->
real OtOpcUaNodeManager, asserting the Area/Line/Equipment folders + the equipment-signal
Variable land in a live OPC UA address space (structure-only). Also covers compose-side
EquipmentTags extraction. The cluster-level deploy + network-browse E2E + scadaproj loader
need the docker-dev fixture (not runnable on this dev box) and are tracked as a follow-up.
Two bundle-review fixes + idempotency coverage:
- CRITICAL: the planner ignored EquipmentTags, so an incremental deploy changing only
equipment tags produced an empty plan and HandleRebuild short-circuited before
materialising them. Add TagId to EquipmentTagPlan + Added/Removed/ChangedEquipmentTags
to Phase7Plan (diffed by TagId, in IsEmpty, driving Apply's needsRebuild) — mirroring
the GalaxyTags treatment.
- IMPORTANT: equipment variable NodeId was the raw driver FullName, which collides across
identical machines (e.g. two PLCs both exposing register 40001) — the second variable
was silently dropped. NodeId is now folder-scoped (parent/Name); FullName stays on
EquipmentTagPlan for the later values-routing milestone.
- Task 4: SDK-backed idempotency test (double-apply -> single variable); restart-safety
confirmed (RestoreApplied reuses the same RebuildAddressSpace -> HandleRebuild path).
- Minor: align composer equipment-tag sort with the artifact decoder (coalesce FolderPath).
Equipment folder DisplayName was the colloquial MachineCode; the live rebuild (artifact
ReadEquipmentNode) + composer now use the UNS level-5 Name segment, matching Area/Line
folders + EquipmentNodeWalker. NodeId stays the logical EquipmentId so browse-path
resolution + ACLs are unaffected.
Add Phase7Applier.MaterialiseEquipmentTags — a sink-based pass (Task-0 decision A) that
ensures each EquipmentTagPlan's Variable (NodeId = FullName) under its existing equipment
folder, nesting any FolderPath as a sub-folder. Wire it into OpcUaPublishActor.HandleRebuild
after the Galaxy pass. Variables start BadWaitingForInitialData; never re-creates equipment
folders (decision #4).
Add EquipmentTagPlan + an init-only EquipmentTags member on Phase7CompositionResult
(mirror of GalaxyTags). Populate it compose-side (Tag.EquipmentId != null AND owning
namespace Kind == Equipment) and artifact-decode-side via BuildEquipmentTagPlans, with
FullName extracted from Tag.TagConfig. Init-only member (not a 7th positional param) so
existing convenience constructors + call sites are untouched.
Implements WS-1/WS-2/WS-4 (+ tests) of the materialization scope: carry
Equipment-namespace tags through the composition/artifact, add a sink-based
MaterialiseEquipmentTags pass to the live rebuild, browse folders by friendly
Name (NodeId stays the logical Id). 6 tasks with dependencies; structure-only
(BadWaitingForInitialData leaves) — live values are the next milestone. Resume
via /executing-plans docs/plans/2026-06-06-equipment-namespace-structure-milestone.md
Equipment-kind namespaces materialise only their Area/Line/Equipment folder
skeleton on deploy, not the signals under them: EquipmentNodeWalker is fully
built + unit-tested but has no production call site, the composition/artifact
drops EquipmentId-bound tags, and no value source is wired (OpcUaClient driver
factory missing; VirtualTag ITagUpstreamSource unregistered). Documents the
gaps, workstreams, value-path options, and a structure-only-first sequencing.
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/.
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.
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.