# Architecture Review — Core Composition Pipeline and Core Libraries

**Date:** 2026-07-08
**Commit:** `9cad9ed0` (master)
**Reviewer scope:**

- `src/Core/ZB.MOM.WW.OtOpcUa.Core` (driver hosting, resilience, stability, authorization trie)
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions` (driver capability interfaces, `PollGroupEngine`, historian seams)
- `src/Core/ZB.MOM.WW.OtOpcUa.Commons` (cluster message contracts, deferred sinks, NodeId scheme, telemetry)
- `src/Core/ZB.MOM.WW.OtOpcUa.Configuration` (EF config persistence, generation-sealed local cache, draft validation)
- `src/Core/ZB.MOM.WW.OtOpcUa.Cluster` (Akka bootstrap, role info, ServiceLevel)
- Plus the address-space composition pipeline itself — `AddressSpaceComposer` / `AddressSpacePlanner` / `AddressSpaceApplier`

**Scope note (location drift).** The review brief places the composition pipeline in
`ZB.MOM.WW.OtOpcUa.Core`, but since the Phase7→AddressSpace rename it lives in
`src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/` (`AddressSpaceComposer.cs`, `AddressSpacePlan.cs`,
`AddressSpaceApplier.cs`), with its tests under `tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/`.
The `Core` project's own `OpcUa/` folder now holds only the retired `EquipmentNodeWalker` and an
unused `GenericDriverNodeManager` (see U-1). The pipeline was reviewed in full regardless.

---

## Architecture Overview

### Composition pipeline (deploy → address space)

The pipeline is a clean three-stage compose/diff/apply design:

1. **`AddressSpaceComposer.Compose`** (`OpcUaServer/AddressSpaceComposer.cs:290`) — a pure static
   projection from EF config entities (`UnsArea`, `UnsLine`, `Equipment`, `DriverInstance`, `Tag`,
   `VirtualTag`, `Script`, `ScriptedAlarm`, `Device`, `Namespace`) into an
   `AddressSpaceComposition`: sorted lists of `UnsAreaProjection`/`UnsLineProjection`/
   `EquipmentNode`/`DriverInstancePlan`/`EquipmentTagPlan`/`EquipmentVirtualTagPlan`/
   `EquipmentScriptedAlarmPlan`. Tag intent (driver `FullName`, native-alarm object, historize
   flags, array shape) is parsed out of the schemaless `Tag.TagConfig` JSON here. VirtualTag
   scripts get `{{equip}}` token substitution and dependency-ref extraction via the shared
   `Commons.Types.EquipmentScriptPaths` helper. Everything is ordinally sorted so the composition
   is deterministic — the foundation of the "byte-parity" contract with the artifact-decode mirror
   in `Runtime/Drivers/DeploymentArtifact.cs`.

2. **`AddressSpacePlanner.Compute`** (`OpcUaServer/AddressSpacePlan.cs:96`) — a pure diff of two
   compositions keyed on stable logical ids, using record value-equality as the changed check
   (with hand-written `Equals`/`GetHashCode` on the plans carrying `IReadOnlyList` members so a
   no-op redeploy diffs empty). Emits Added/Removed/Changed sets per entity class plus UNS
   folder renames. `AddressSpacePlan.IsEmpty` is the short-circuit gate `OpcUaPublishActor` uses.

3. **`AddressSpaceApplier.Apply`** (`OpcUaServer/AddressSpaceApplier.cs:73`) — the only
   side-effecting stage. Decides between a **full structural rebuild** (any topology change) and a
   **surgical in-place update** (`ISurgicalAddressSpaceSink.UpdateTagAttributes` /
   `UpdateFolderDisplayName` for whitelisted node-irrelevant deltas — the F10b optimization),
   falling back to rebuild when the sink lacks the capability or any surgical call fails. After
   the address-space work it dispatches two non-blocking hooks: fire-and-forget historian tag
   provisioning (`IHistorianProvisioning.EnsureTagsAsync`) and the historized-ref delta feed to
   the continuous-historization recorder (`IHistorizedTagSubscriptionSink.UpdateHistorizedRefs`).
   Separate `Materialise*` passes (Hierarchy / EquipmentTags / VirtualTags / ScriptedAlarms /
   DiscoveredNodes) re-derive nodes from the composition after a rebuild; NodeIds are
   folder-scoped (`Commons.OpcUa.EquipmentNodeIds`: `{equipment}/{folder}/{name}`), never the
   driver `FullName`, to avoid collisions across identical machines.

The sink boundary (`Commons.OpcUa.IOpcUaAddressSpaceSink`) keeps the whole pipeline SDK-free:
production binds `SdkAddressSpaceSink` via the late-swap `DeferredAddressSpaceSink` (actors resolve
the wrapper at DI time, the OPC UA hosted service swaps the real sink in after `StandardServer`
starts); dev/tests bind the `NullOpcUaAddressSpaceSink` no-op.

### Supporting Core libraries

- **`Core/Hosting`** — `DriverFactoryRegistry` (driver-type → factory), `DriverFactoryRegistryAdapter`
  (v1 registry → v2 `IDriverFactory`), `DriverHost` (id → `IDriver` lifecycle registry).
- **`Core/Resilience`** — Polly pipelines cached per `(DriverInstanceId, HostName, Capability)`
  (`DriverResiliencePipelineBuilder`), executed via `CapabilityInvoker` (with the
  non-idempotent-write no-retry override), fanned out per host by `AlarmSurfaceInvoker`, observed
  by `DriverResilienceStatusTracker` for Admin `/hosts`.
- **`Core/Stability`** — `MemoryTracking` (median-baseline soft/hard breach), `MemoryRecycle` and
  `ScheduledRecycleScheduler` (Tier C recycle via `IDriverSupervisor`), `WedgeDetector`
  (demand-aware stall detection).
- **`Core/Authorization`** — generation-sealed `PermissionTrie` per `(ClusterId, GenerationId)`
  cached in `PermissionTrieCache` (CAS-pruned), walked by `TriePermissionEvaluator` against
  per-session `UserAuthorizationState` (freshness window + fail-closed staleness ceiling).
- **`Commons`** — immutable record message contracts for the Akka DPS topics (deploy dispatch/ack,
  alerts `AlarmTransitionEvent`, redundancy state, admin ops, script log), strongly-typed ids
  (`DeploymentId`, `RevisionHash`, `CorrelationId`, `NodeId`), the deferred sink/publisher pair,
  `EquipmentScriptPaths` (shared script-path/dependency extraction), and `OtOpcUaTelemetry`
  (central Meter/ActivitySource).
- **`Configuration`** — `OtOpcUaConfigDbContext` (26 entities, logical-id unique indexes, JSON
  check constraints, RowVersion concurrency), the generation-sealed LiteDB fallback cache
  (`GenerationSealedCache` + atomic `CURRENT` pointer, fail-closed on corruption),
  `ResilientConfigReader` (timeout→retry→cache-fallback with secret-scrubbed logging),
  `DraftValidator` (all-errors-in-one-pass pre-publish rules).
- **`Cluster`** — `AddOtOpcUaCluster` / `WithOtOpcUaClusterBootstrap` (Akka.Hosting bootstrap with
  embedded HOCON + Serilog logger wiring), `ClusterRoleInfo` (lock-guarded role topology snapshot
  fed by a subscriber actor), `ServiceLevelCalculator` (pure 0–255 tiering), `RoleParser`.

Dependency direction: `Core.Abstractions` (leaf) ← `Configuration` ← `Core`; `Commons` (leaf,
Akka+Audit packages) ← `Cluster`. The composer/applier in `OpcUaServer` reference both
`Configuration` (entities) and `Commons` (sink contracts).

---

## Findings

### 1. Stability

#### S-1 — Applier swallows every sink failure; a deploy is reported applied even when the address space is broken — **High**

`AddressSpaceApplier` wraps every sink call in catch-all "Safe" helpers that log and continue:
`SafeEnsureFolder`/`SafeEnsureVariable` (`AddressSpaceApplier.cs:612–622`), `SafeRebuild`
(`AddressSpaceApplier.cs:373–383`), `SafeWriteAlarmCondition`/`SafeMaterialiseAlarmCondition`
(`AddressSpaceApplier.cs:677–687`). `AddressSpaceApplyOutcome` (`AddressSpaceApplier.cs:691`)
carries only Added/Removed/Changed counts and `RebuildCalled` — **no failure count**. If
`RebuildAddressSpace()` throws, `rebuilt` is still reported `true` (`AddressSpaceApplier.cs:150–153`)
and the outcome flows back to the deploy coordinator as a success; the deployment seals while the
running server holds a partially-materialised (or entirely stale) address space. Individual
`EnsureVariable` failures during a materialise pass likewise vanish into per-node Warnings.

The never-fail-a-deploy posture is defensible for the *detached* hooks (provisioning, historized-ref
feed — both correctly isolated), but structural materialisation failures are the deploy's core
contract.

**Recommendation:** add `FailedNodes`/`RebuildFailed` to `AddressSpaceApplyOutcome`, propagate into
the `ApplyAck`/`DeploymentFailed` decision and the audit log, and escalate `SafeRebuild` failure to
at least a degraded ack. Tests should assert the failure surface (the fixture sink can throw).

#### S-2 — `PollGroupEngine.Unsubscribe` blocks the calling thread up to 5 s per subscription — **Medium**

`StopState` does a synchronous `task.Wait(TimeSpan.FromSeconds(5))`
(`Core.Abstractions/PollGroupEngine.cs:99–112`). `Unsubscribe` is the teardown path drivers call
from OPC UA subscription-management callbacks and actor message handlers; a reader stuck in a slow
network call turns every unsubscribe into a 5-second stall of the calling thread (and N stalls when
tearing down N subscriptions serially). `DisposeAsync` gets this right (parallel cancel, awaited
`WhenAll` with one shared timeout, `PollGroupEngine.cs:213–236`); the single-subscription path does
not.

**Recommendation:** add an `UnsubscribeAsync` (or make `Unsubscribe` cancel + hand the await to a
background drain), keeping the "no callback after teardown" guarantee via the existing
LoopTask/CTS-dispose ordering.

#### S-3 — `PollGroupEngine.Subscribe` has no disposed guard — **Low**

`Subscribe` racing `DisposeAsync` can insert a fresh `SubscriptionState` after the dispose loop has
snapshotted `_subscriptions.Values` (`PollGroupEngine.cs:73–84` vs `213–236`), leaking a live poll
loop with no owner. Same pattern in `DriverHost.RegisterAsync` vs `DisposeAsync`
(`Core/Hosting/DriverHost.cs:53–71` vs `92–106`): a registration that wins the race after the
snapshot+clear is initialized but never shut down. Both are shutdown-window races, unlikely in
practice but cheap to close with a `_disposed` flag checked inside the lock / before insert.

#### S-4 — `ScheduledRecycleScheduler` catch-up recycle storm after a stall — **Low**

`TickAsync` advances `_nextRecycleUtc` by exactly one interval per fire
(`Core/Stability/ScheduledRecycleScheduler.cs:72–84`). If the host was suspended (VM pause, long
outage) across K intervals, the next K ticks each trigger a full Tier C process recycle
back-to-back. **Recommendation:** fast-forward `_nextRecycleUtc` past `utcNow` after a fire.

#### S-5 — `GenericDriverNodeManager` re-walk teardown is not synchronized — **Low**

`BuildAddressSpaceAsync` tears down the previous alarm forwarder and clears `_alarmSinks`
non-atomically (`Core/OpcUa/GenericDriverNodeManager.cs:58–71`); a concurrent driver alarm event
during the window between unsubscribe and re-subscribe is dropped, and two concurrent Build calls
would interleave badly. Currently moot because the class has no production caller (see U-1) — fold
this into whatever decision is made there.

**Positive observations.** The failure-mode engineering in `Configuration/LocalCache` is exemplary:
`GenerationSealedCache` writes temp-file + atomic `File.Replace` pointer swaps, fails closed on any
corruption rather than silently serving an older generation (`GenerationSealedCache.cs:111–155`);
`LiteDbConfigCache` documents and defuses two real LiteDB concurrency traps (global `BsonMapper`
races, find-then-insert upsert races) (`LiteDbConfigCache.cs:16–36`). `ResilientConfigReader`
correctly distinguishes SQL-command `TaskCanceledException` from genuine caller cancellation
(`ResilientConfigReader.cs:117–138`) and scrubs connection-string secrets from log messages.
`PermissionTrieCache.Prune` is a correct reference-equality CAS loop (`PermissionTrieCache.cs:73–102`).
The applier's two historian hooks are properly detached (continuation observes the task, never
`.Result` on a fault — `AddressSpaceApplier.cs:259–286`). `PollGroupEngine` classifies fatal
exceptions and hardens the `onError` callback (`PollGroupEngine.cs:153–169`).

### 2. Performance

#### P-1 — `Compose` parses each tag's `TagConfig` JSON four times — **Medium**

`ExtractTagFullName`, `ExtractTagAlarm`, `ExtractTagHistorize` and `ExtractTagArray` each do their
own `JsonDocument.Parse` of the same blob per tag (`AddressSpaceComposer.cs:418–433` calling
`:536–686`). A compose over a 10k-tag fleet performs 40k JSON parses (plus the same again on the
artifact-decode side). Compose runs on every deploy and every diff baseline, not on the data hot
path, so this is a scalability tax rather than a latency bug today.

**Recommendation:** parse once per tag into a small `TagConfigIntent` record (which also collapses
the byte-parity duplication — see C-1).

#### P-2 — Per-operation allocations on the authorization hot path — **Medium**

`PermissionTrie.CollectMatches` allocates a fresh `HashSet<string>` from the session's LDAP groups
plus a `List<MatchedGrant>` on every call (`Core/Authorization/PermissionTrie.cs:42–45`), and
`TriePermissionEvaluator.Authorize` runs per node-operation. A recursive browse or a large
CreateMonitoredItems batch turns this into per-node garbage. `UserAuthorizationState.LdapGroups` is
immutable per membership refresh — the case-insensitive set can be computed once per session
version and reused.

#### P-3 — `PollGroupEngine`: one background task + independent timer per subscription, no cross-subscription batching — **Medium**

Each `Subscribe` spawns a dedicated `Task.Run` loop (`PollGroupEngine.cs:73–84`). Drivers that map
each OPC UA monitored-item group (or each poll group) to a subscription get linear task/timer
growth, and two subscriptions polling overlapping tag sets at the same interval issue duplicate
protocol reads — the engine has no interval-bucketed scheduler or read coalescing. Fine at dozens
of subscriptions; a scaling wall at hundreds per driver instance. Worth a note in the driver-facing
docs at minimum; an interval-bucketed shared loop is the structural fix if tag counts grow.

#### P-4 — Per-write allocation in the non-idempotent write path — **Low**

`CapabilityInvoker.ExecuteWriteAsync` builds a new options record + `Dictionary` per non-idempotent
write (`Core/Resilience/CapabilityInvoker.cs:135–152`) even though `GetOrCreate` keys only on
`(id, host, capability)` and ignores the options after the first build — the comment even cites the
"1% pipeline budget". Cache the no-retry snapshot per options generation, or key the fast path on a
precomputed pipeline.

**Positive observations.** Pipeline resolution is lock-free `ConcurrentDictionary` reads
(`DriverResiliencePipelineBuilder.cs:58–71`). The planner's diff is O(N) dictionary passes with
deterministic sorted outputs. The surgical-apply path exists precisely to avoid the expensive full
rebuild + subscription teardown for attribute-only edits, and its whitelist-via-`with`-expression
technique (`AddressSpaceApplier.cs:630–663`) is future-field-safe (unknown new fields force the
safe rebuild). Telemetry instruments are no-op until a listener attaches (`OtOpcUaTelemetry.cs`).

### 3. Conventions

#### C-1 — "Byte-parity by convention": TagConfig parsing replicated in four projects with comment-enforced sync — **High**

`ExtractTagFullName` (and friends) exist as deliberate copies in:

- `OpcUaServer/AddressSpaceComposer.cs:536–551` (+ `ExtractTagAlarm`/`ExtractTagHistorize`/`ExtractTagArray`/`TryExtractDeviceHost`)
- `Runtime/Drivers/DeploymentArtifact.cs` (the artifact-decode mirror, per its own comments)
- `Core/OpcUa/EquipmentNodeWalker.cs:193–208`
- `Configuration/Validation/DraftValidator.cs:60–71` (a fourth "small local copy, consistent with
  this codebase where the composer keeps its own")

Each copy carries a "MUST parse identically (byte-parity)" comment. The equality of the deployed
address space across the live-compose and artifact-decode seams — and therefore the correctness of
the diff/no-op-redeploy behavior — rests on humans keeping four JSON parsers in sync. The codebase
already demonstrated the fix once: `EquipmentScriptPaths` (`Commons/Types/EquipmentScriptPaths.cs`)
was created exactly to de-duplicate the `ctx.GetTag` extraction "those two seams used to duplicate".

**Recommendation:** move the TagConfig/DeviceConfig intent parsing into `Commons` (it has no EF or
SDK dependency; every current copy's host project already references Commons or could) as a single
`TagConfigIntent.Parse(string)` — also resolving P-1 — and keep one cross-seam parity test instead
of N replicated implementations.

#### C-2 — Core depends on Configuration: EF entities and enums are the domain model — **Medium**

`ZB.MOM.WW.OtOpcUa.Core.csproj` references `Configuration`; `PermissionTrie` consumes
`Configuration.Enums.NodePermissions`/`NodeAclScopeKind` (`Core/Authorization/PermissionTrie.cs:1`),
`EquipmentNodeWalker` consumes `Configuration.Entities` directly, and the composer's plan records
are projections of EF entities. There is no separate domain layer — persistence types *are* the
model. This is a deliberate, consistent choice (and the plan records do decouple the applier), but
it means: (a) any EF-driven entity change ripples straight into authorization and composition
logic; (b) `Configuration` (with EF Core, SqlServer, LiteDB, DataProtection packages) is pulled
into anything referencing Core. Flagging as accepted-risk to document rather than refactor —
new code should keep preferring the plan-record boundary the composer established.

#### C-3 — `DraftValidator` re-derives the NodeId scheme and hard-codes a driver-type string — **Medium**

`ValidateNoEquipmentSignalNameCollision` re-implements the folder-scoped NodeId key by hand
(`Configuration/Validation/DraftValidator.cs:75–97`) because `Configuration` cannot reference
`Commons.OpcUa.EquipmentNodeIds` (dependency direction) — a third copy of the
`{equipment}/{folder}/{name}` scheme whose drift would silently break the collision check.
`ValidateGalaxyTagFullName` hard-codes `dtype != "GalaxyMxGateway"`
(`DraftValidator.cs:41–55`) rather than a shared driver-type constant. Both are symptoms of the
same missing shared-contract home as C-1; a Commons-level scheme/constants type (or moving
`EquipmentNodeIds` down to a project Configuration can see) fixes both.

#### C-4 — Composer violates its own purity contract with `Trace.TraceWarning` — **Low**

The class doc says "Same inputs → same outputs, no logging" (`AddressSpaceComposer.cs:280–283`),
but the dangling-predicate-script skip emits `Trace.TraceWarning` (`AddressSpaceComposer.cs:493–496`)
— the only `System.Diagnostics.Trace` usage in a Serilog/`ILogger` codebase, invisible in the
production sinks. Either return skipped-alarm ids in the composition for the caller to log, or
accept an injected logger and fix the doc.

#### C-5 — Inconsistent duplicate-key defensiveness inside `Compose` — **Low**

`deviceHostById` is built with a deliberate last-wins `foreach` and a comment explaining why
`ToDictionary` (throw-on-dupe) would diverge from the decode side (`AddressSpaceComposer.cs:352–364`),
yet three lines later `driversById`/`namespacesById`/`scriptsById` use plain `ToDictionary`
(`AddressSpaceComposer.cs:401–402, 453`) — a duplicate logical id (only possible on DB-constraint
bypass) crashes the whole compose. Pick one posture; the defensive one is already argued for.

#### C-6 — Vestigial `Akka` package reference in Commons — **Low**

`Commons.csproj` references the `Akka` package but no Commons source uses an Akka type (only doc
comments mention actors). Harmless today because every consumer is Akka-hosted anyway, but it
contradicts Commons' role as the dependency-light contracts layer. Remove it.

#### C-7 — Stale scaffold-era XML docs on load-bearing classes — **Low**

`AddressSpaceApplier`'s class doc still describes the F14 scaffold ("For now we record the work",
"the SDK adapter that lands in F10b will decide…" — `AddressSpaceApplier.cs:7–26`) although both
features shipped. `DriverHost`'s doc promises "per-process isolation for Tier C … implemented in
Phase 2 via named-pipe RPC" (`Core/Hosting/DriverHost.cs:5–10`), an architecture superseded by the
out-of-repo mxaccessgw gateway. In a codebase whose XML documentation is otherwise a genuine asset
(and evidently relied on), stale top-of-class narratives are actively misleading.

**Positive observations.** Naming and structure are highly consistent (logical-id + RowVersion
entity pattern, `Null*`/`Deferred*` sink pattern, options records with `Validate()`, message
records per DPS topic). Zero `TODO`/`HACK`/`FIXME`/`Obsolete` markers across all five projects. The
DbContext matches a documented schema with an introspection compliance test
(`SchemaComplianceTests`). The `DeferredAddressSpaceSink` explicitly forwards the surgical
capability with a comment memorializing the F10b prod-inertness bug (`DeferredAddressSpaceSink.cs:52–70`)
— institutional memory encoded where the next person will trip.

### 4. Underdeveloped Areas

#### U-1 — Dead/dormant code retained in Core: `GenericDriverNodeManager`, `EquipmentNodeWalker`, `TryParseRelayBody` — **Medium**

- `GenericDriverNodeManager` (`Core/OpcUa/GenericDriverNodeManager.cs`) has **no production
  references** — only its own test file. The production path is the composer→applier→sink chain.
- `EquipmentNodeWalker` (`Core/OpcUa/EquipmentNodeWalker.cs:160–165`) self-declares "retained for
  unit-test support only; real server deployments never invoke Walk" — ~280 lines plus
  `IdentificationFolderBuilder` and a full test suite exercising a code path that cannot run in
  production. Its `ExtractFullName` copy is also one of the four C-1 duplicates.
- `EquipmentScriptPaths.TryParseRelayBody` (`Commons/Types/EquipmentScriptPaths.cs:164–172`)
  serviced the relay→alias converter that was deleted when Galaxy became a standard driver
  (2026-06-12); only tests call it now.

Dormant code with green tests is worse than deleted code: it passes every gate while silently
diverging from the live path. **Recommendation:** delete the walker + GDNM + relay parser (git
preserves them), or, if the walker must survive as a test fixture, move it into the test project.

#### U-2 — The entire Tier C recycle machinery has no `IDriverSupervisor` implementation — **Medium**

`MemoryRecycle`, `ScheduledRecycleScheduler`, and `DriverResilienceStatusTracker.RecordRecycle`
all drive `IDriverSupervisor.RecycleAsync`, but a repo-wide search finds **zero concrete
implementations** of `IDriverSupervisor` (`Core.Abstractions/IDriverSupervisor.cs` + the two
Stability consumers are the only references). Since the out-of-process driver story moved to the
external mxaccessgw/HistorianGateway sidecars, no in-repo driver is Tier C, so the recycle path is
compiled, tested, and unreachable. Either document it as speculative infrastructure for a future
tier-migration workflow (the `MemoryRecycle` doc hints at one) or prune it alongside U-1.

#### U-3 — Continuous-historization interest set: delta feed landed, initial set still empty — **Medium (known-limitation debt)**

The applier now pushes add/remove historized-ref deltas per deploy
(`AddressSpaceApplier.cs:310–353` → `IHistorizedTagSubscriptionSink`), which is the convergent
design for *subsequent* deploys — but per the documented KNOWN LIMITATION 2 (CLAUDE.md), the
recorder is still spawned with an empty ref set, so after a process restart the recorder historizes
nothing until the next deploy produces a delta. The seam contract
(`Core.Abstractions/Historian/IHistorizedTagSubscriptionSink.cs`) supports the fix; the missing
piece is a full-set replay at spawn/rebuild (e.g. have the applier feed the *complete* current set
on `RebuildCalled`, not just the diff). This is the highest-leverage half-finished feature in the
subsystem.

#### U-4 — Additive-only ACL model (no Deny) is a documented v2.0 gap — **Low**

`PermissionTrie` is explicitly pure-union ("no explicit Deny in v2.0",
`Core/Authorization/PermissionTrie.cs:13–17`): a broad cluster-root grant cannot be carved back at
a sub-scope. Acceptable for the current flat-role deployments (AdminUI roles are global by design),
but it bounds how fine-grained OT authorization can get without a trie-walk semantics change —
worth keeping visible in the security docs.

#### U-5 — Test coverage is strong but unevenly distributed — **Low**

Test method counts: Core.Tests 208, Configuration.Tests 94, Core.Abstractions.Tests 65,
Commons.Tests 45, Cluster.Tests 21. The composition pipeline itself is thoroughly covered (10+
dedicated files in `tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpace*`). Gaps:
`ClusterRoleInfo` — the only concurrency-bearing class in the Cluster project (lock + subscriber
actor + event fan-out) — has no test (Cluster.Tests covers only the three pure classes);
`DriverHost` dispose/register races (S-3) and the applier failure surface (S-1) are untested
because the surface doesn't exist. An Akka TestKit harness for `ClusterRoleInfo` leader-change
sequencing would close the riskiest untested seam.

---

## Maturity Ratings

| Dimension | Rating (1–5) | Justification |
|---|---|---|
| Stability | **4** | Deliberate, well-commented failure-mode engineering throughout (fail-closed caches, atomic pointer swaps, detached hooks, fatal-exception classification); docked for the applier's swallow-everything success reporting (S-1) and the blocking unsubscribe (S-2). |
| Performance | **3** | Deploy-path costs are deterministic and diffed to near-zero for no-op redeploys, and the surgical-apply path avoids rebuild churn; but hot paths carry avoidable per-operation allocations (P-2), redundant JSON parsing scales 4× with tag count (P-1), and the poll engine is task-per-subscription with no coalescing (P-3). |
| Conventions | **4** | Exceptionally consistent naming, patterns, and XML docs with zero TODO debt; docked for the systemic replicate-and-comment "byte-parity" duplication (C-1/C-3) and the Core→Configuration persistence coupling (C-2). |
| Underdeveloped areas | **3** | The live pipeline is feature-complete and well-tested, but Core retains three dead/dormant code paths with green tests (U-1), a fully-built-but-unreachable Tier C recycle subsystem (U-2), and the known continuous-historization initial-set gap (U-3). |
