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.
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.
Independently re-ran the D.1 alarm-source smoke against the live gateway
(10.100.0.48:5120) to back the native MxAccess alarm-event claim with a
fresh empirical run, not just the original 2026-05-29 capture.
The 2026-04-30 alarm plan banners claimed worker-side native alarm
subscription was blocked on a COM-bitness finding. That's stale: the
mxaccessgw .NET client now has true MxAccess alarm-event support, and a
live StreamAlarms check (+ new Skip-gated GatewayGalaxyAlarmFeedLiveTests
through the lmxopcua consumer) confirms native alarms — operator comment,
category, severity, timestamps — flow end-to-end. Reconcile both plan docs
to reality and add docs/plans/alarms-d1-smoke-artifact.md as the D.1
alarm-source deliverable. Historian-write live smoke + full server->A&C
round-trip remain (Windows parity rig only).
Driver collection editors (modal-per-row shared shell), resilience typed
form, editable DB-backed LDAP->role map (global roles, live on next
sign-in), and stale-comment/note cleanup. Roles intentionally global —
no per-cluster permissions.
ASP.NET Core's cookie-handler IsAjaxRequest heuristic only checks
X-Requested-With (not Accept). Drop the third test (Accept: application/json
was assumed to → 401 but actually → 302) and the Location.ShouldBeNull
assertion on the XHR test (framework still writes Location alongside 401;
clients ignore it). Renamed _ajax_ → _xhr_ for accuracy. Design doc
updated to match.
5 tasks following Section 6 of the approved design (bc4fce5). Tasks 3 and 4
parallelizable. Each task carries Classification + Estimated implement time
+ Parallelizable-with metadata for subagent dispatch.
Removes the JwtBearer parallel scheme + non-redirect 401 challenge that left
browsers staring at Chrome's HTTP_RESPONSE_CODE_FAILURE page on protected
GETs. JWT keeps minting (cookie payload only); cookie config flows through
the existing-but-unused OtOpcUaCookieOptions via PostConfigure (same pattern
ScadaBridge uses).
18-task plan following Section 9 of the approved design. Phases 3 & 4
parallelizable. Each task carries Classification + Estimated implement
time + Parallelizable-with metadata to drive subagent dispatch.
Approved design for the deferred follow-up from PR #f9fc7dd's driver-pages
work. Lazy tree browse via per-driver IDriverBrowser registered in AdminUI
DI, sessions held in-process with TTL reaper. Detailed sequencing for the
writing-plans handoff is in section 9.
Records final commit hashes + notes per task. Persistence file mirrors
the 43-commit branch state so future sessions can resume from the
correct checkpoint via /superpowers-extended-cc:executing-plans.
- DriverTestConnectE2eTests: 3 scenarios (sim/wrong-port/black-hole)
against the Modbus Docker fixture. Sim + wrong-port skip if fixture
unreachable; black-hole uses ModbusDriverProbe directly (no fixture).
- DriverReconnectE2eTests: message round-trip through AdminOperationsActor
cluster singleton — Ok=true + audit write, without live driver side effect.
- DriverStatusHubE2eTests: bridge-mocked fallback — spawns
DriverStatusSignalRBridge in the harness ActorSystem with a mock
IHubContext, publishes DriverHealthChanged to the driver-health DPS
topic, asserts store upsert + hub SendAsync call.
- DockerFixtureAvailability helper: TCP-connect probe for skip guards.
- Moq 4.20.72 added to central package management for hub mocking.
- Design doc §8.3 replaced with concrete pre-ship operator runbook.
- RestartDriver / ReconnectDriver messages + AdminOperationsActor
handlers (broadcast via driver-control DPS topic; audited via
ConfigEdits).
- DriverHostActor subscribes to driver-control; locates the
matching child DriverInstanceActor and stops+respawns it
(Restart) or sends it a ForceReconnect internal message
(Reconnect — re-enters Reconnecting state without full stop).
DriverInstanceSpec constructor call uses named args to handle
the full 6-parameter signature.
- New DriverOperator authorization policy mapped to DriverOperator
or FleetAdmin role; documented in docs/security.md. Map LDAP
group via GroupToRole (e.g. "ot-driver-operator": "DriverOperator").
- DriverStatusPanel renders Reconnect + Restart buttons when the
user holds the DriverOperator policy (hidden otherwise). Restart
requires an in-page Razor confirm block (no JS confirm, keeps
SignalR event loop unblocked). Both buttons show a spinner and
are disabled during in-flight; result chip auto-clears after 8s.
Username sourced from AuthenticationStateProvider.
Reconnect resolves to "ForceReconnect" (re-enter Reconnecting,
not full stop+respawn) — transport drops and retries while actor
and in-memory state are preserved. All DriverInstanceActor states
handle ForceReconnect safely (no-op when already in transition).
Replaces the generic JSON-blob DriverEdit page with typed per-driver
pages (all 9 drivers), Test Connect, live status panel with
Reconnect/Restart, and a per-driver tag/address picker. Live OPC UA +
Galaxy browse explicitly deferred to a follow-up.
OtOpcUaNodeManager + SdkAddressSpaceSink: the v2 IOpcUaAddressSpaceSink
seam now has a production adapter against a real Opc.Ua.Server
CustomNodeManager2. Writes through OpcUaPublishActor's sink materialise
as real OPC UA Variable updates that subscribed clients see via the
standard ClearChangeMasks notification path.
OtOpcUaNodeManager (CustomNodeManager2):
- Owns a ConcurrentDictionary<string, BaseDataVariableState> under a
single namespace (https://zb.com/otopcua/ns) hung off Objects/.
- WriteValue lazy-creates the variable on first write, sets Value +
StatusCode (mapped from OpcUaQuality severity bits) + SourceTimestamp,
then ClearChangeMasks to notify subscribers.
- WriteAlarmState surfaces a [active, acknowledged] pair on a
dedicated node id — full AlarmConditionState/event firing comes
with #85 F14b (EquipmentNodeWalker SDK integration).
- RebuildAddressSpace tears down every registered variable + clears
the dictionary so the next write-pass starts fresh.
- Address-space root folder is materialised in CreateAddressSpace.
SdkAddressSpaceSink: thin IOpcUaAddressSpaceSink → OtOpcUaNodeManager
bridge. Production DI binding (#108) constructs this once the host's
StandardServer has booted.
OtOpcUaSdkServer (StandardServer subclass): overrides
CreateMasterNodeManager to inject OtOpcUaNodeManager via the
MasterNodeManager additionalManagers ctor. NodeManager property
exposes the live instance so OpcUaApplicationHost callers can wrap
it in a sink.
Tests: OpcUaServer 20 -> 24 (+4):
- WriteValue creates + updates variables in the manager
- WriteAlarmState creates a node distinct from value writes
- RebuildAddressSpace clears everything; subsequent writes start fresh
- NullOpcUaAddressSpaceSink no-op sanity
Each test boots a real OpcUaApplicationHost on a free port with the
SDK certificate auto-create flow (F13a) intact — full integration
slice on macOS.
All 6 v2 test suites green: 167 tests passing.
F10 status updated to reflect SDK binding shipped. Residuals:
- #109 OpcUaPublishActor.RebuildAddressSpace → Phase7Applier wiring
- #108 Host DI default to SdkAddressSpaceSink when hasDriver
- #85 F14b EquipmentNodeWalker integration (proper AlarmConditionState
+ folder hierarchy)
- IServiceLevelPublisher SDK binding (writes Server.ServiceLevel node)
End-to-end data path is now wired on the read side: driver subscriptions
fire AttributeValuePublished → DriverHostActor → DependencyMuxActor →
DependencyValueChanged to every interested VirtualTagActor. Previously
the publish hit a dead-letter at the host.
DependencyMuxActor:
- Per-node fan-out router. Maintains tagRef → Set<IActorRef> with a
reverse subscriber → refs index so unregister/replace are O(refs).
- Watches subscribers; Terminated triggers automatic unregister so
dead virtual-tag actors stop receiving publishes.
- Re-register replaces the prior interest set — no stale-ref leaks
on actor restart.
- Drops publishes for refs with no interested subscribers.
VirtualTagActor:
- New Props params: dependencyRefs + mux ActorRef.
- PreStart sends RegisterInterest to the mux; PostStop sends
UnregisterInterest. Default both null so older callers stay quiet.
DriverHostActor:
- New dependencyMux Props param. Steady + Applying states now
receive AttributeValuePublished from their DriverInstance children
and forward to the mux. Null mux is a no-op (dev/Mac).
ServiceCollectionExtensions:
- WithOtOpcUaRuntimeActors spawns DependencyMuxActor before
DriverHostActor and threads its ActorRef into the host's Props.
New DependencyMuxActorKey + DependencyMuxActorName.
Tests: Runtime 57 -> 63 (+6):
- Mux forwards to only subscribers interested in each ref
- Publish for unregistered ref is dropped silently
- Unregister stops forwarding
- Re-register replaces prior interest set
- VirtualTagActor PreStart registration drives end-to-end eval
(uses AwaitAssert to race-safely settle the PreStart Tell)
- DriverHostActor forwards AttributeValuePublished through to mux
All 6 v2 test suites green: 163 tests passing.
F8 (#79) state updated — dep subscribe seam shipped, Core.VirtualTags
production engine binding (compile + ITagUpstreamSource subscribe) is
the residual.
ScriptedAlarmActor now survives actor restart: PreStart loads from
the configured store + restores in-memory state; every Transition()
fires a fire-and-forget save. ActiveState still re-derives from the
evaluator on first tick (Phase 7 decision #14), but Acked state +
lastAckUser persist verbatim so operators don't re-ack across an
outage.
Three pieces:
- IAlarmActorStateStore seam in Commons.Engines, with the
AlarmActorStateSnapshot record (alarmId / state / lastTransitionUtc
/ lastAckUser) and NullAlarmActorStateStore default.
- EfAlarmActorStateStore in Runtime.ScriptedAlarms — production
adapter over the existing ScriptedAlarmState table in ConfigDb.
Maps the actor's 3-state enum to the table's AckedState column
(Active⇒Unacknowledged, Acknowledged⇒Acknowledged, Inactive⇒
Acknowledged). Concurrency conflicts are logged + dropped — the
next transition writes again.
- ScriptedAlarmActor PreStart load (async, piped back as
StateRestored) + Transition save. New Props overload takes the
store; default is NullAlarmActorStateStore so tests stay quiet.
Tests: Runtime 52 -> 57 (+5):
- Transition writes Active then Acknowledged snapshots with
lastAckUser populated
- PreStart with persisted Active state restores so a subsequent
AcknowledgeAlarm fires (not ignored as it would be from Inactive)
- Empty store boots Inactive (AcknowledgeAlarm correctly ignored)
- EfAlarmActorStateStore Save + Load round-trips via in-memory EF
- Load for unknown alarmId returns null
All 6 v2 test suites green: 157 tests passing.
Closes#112. F9 (#80) remaining residual is predicate binding to
Core.ScriptedAlarms.ScriptedAlarmEngine — split as F9b in tasks JSON.
Three pieces landed in one batch, closing F7-residual + Host DI #106:
Runtime/DriverInstanceActor:
- Subscribe / Unsubscribe message contracts; the Connected state
handles them via IDriver.ISubscribable. On every OnDataChange
event the actor publishes AttributeValuePublished to its parent
(DriverHostActor → OpcUaPublishActor). OPC UA StatusCode is
mapped to the 3-state OpcUaQuality enum via severity bits
(00=Good, 01=Uncertain, 10/11=Bad).
- DetachSubscription tears the handler off the driver on
DisconnectObserved, Unsubscribe, and PostStop so a stale handler
never pushes to a dead actor.
- WriteAttribute now dispatches IWritable.WriteAsync (batch of one)
with a 5s CancellationTokenSource; status-code propagated to
WriteAttributeResult on non-Good results.
Host:
- New ProjectReferences to Core + every cross-platform driver
assembly (AbCip/AbLegacy/FOCAS/Galaxy/Modbus/S7/TwinCAT).
Galaxy is net10 (gRPC client to mxaccessgw); the COM-bound net48
Wonderware Historian driver stays out of the Host's reference
closure — its .Client gRPC wrapper is what binds for historian
needs.
- New DriverFactoryBootstrap.AddOtOpcUaDriverFactories() registers
a singleton DriverFactoryRegistry, invokes each driver's
Register(registry, loggerFactory), and binds IDriverFactory to
DriverFactoryRegistryAdapter. Replaces the F7 NullDriverFactory
default so deploys actually materialise real IDriver instances
on driver-role nodes. ShouldStub() still gates per-platform
behaviour at spawn time.
- Program.cs wires AddOtOpcUaDriverFactories() before AddAkka so
the runtime extension can resolve IDriverFactory from DI.
Tests: Runtime 46 -> 52 (+6):
- Write returns success when StatusCode = Good
- Write propagates non-Good status code in failure Reason
- Subscribe forwards OnDataChange to parent as AttributeValuePublished
- Quality translation: Uncertain (0x40...) and Bad (0x80...)
- Subscribe against non-ISubscribable returns failure
- DisconnectObserved detaches handler so late events are dropped
All 6 v2 test suites green: 152 tests passing.
Closes F7. F7-residual sub-tasks #110 (subscribe) and #111 (write)
both shipped. Host DI binding #106 shipped.