# Architecture Review — Core Composition Pipeline and Core Libraries

**Date:** 2026-07-12
**Commit:** `f6eaa267` (master, clean tree)
**Note:** This updates the 2026-07-08 review at `9cad9ed0`. Since that baseline the arch-review
remediation branches merged to master; the pieces landing in this domain are: the
`IDriverCapabilityInvoker`/`IDriverCapabilityInvokerFactory` seam in Core.Abstractions +
`DriverCapabilityInvokerFactory` in Core (`bacea1a4`, task #10), per-instance ResilienceConfig
read-path plumbing through the deploy artifact (`75403caa`, task #13), the reflection-exhaustive
deferred-sink forwarding guard in Commons.Tests (`a65c2ced`, task #6), the typed SBR option in
Cluster (`a81dea10` + premise correction `eaf78aad`), the OTOPCUA0001 analyzer wiring (`f0082af5`),
`IScriptCacheOwner` in Commons (`7fd44f0f`), and a fixdocs XML-doc sweep (`3b5ef439`).

**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, the new invoker seam)
- `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, SBR options)
- 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 still holds only the retired `EquipmentNodeWalker` and an
unused `GenericDriverNodeManager` (see U-1). The pipeline was reviewed in full regardless. The
composition pipeline itself received **zero code changes** in the 9cad9ed0→f6eaa267 window; the
domain's delta is concentrated in `Core/Resilience`, `Core.Abstractions`, `Cluster`, and the Runtime
dispatch layer that consumes the new seam.

---

## Prior-finding status (9cad9ed0 → f6eaa267)

Every prior finding was re-verified against the current code, not the STATUS.md claims.

| ID | One-line description | Verified status |
|---|---|---|
| S-1 | Applier swallows every sink failure; deploy reported applied even when broken | **STILL OPEN** — `Safe*` helpers unchanged (`AddressSpaceApplier.cs:373–383, 612–622`); `AddressSpaceApplyOutcome` (`:691`) still has no failure fields; `rebuilt = true` still set after `SafeRebuild()` (`:148–153`). STATUS.md itself lists 01/S-1 under "Suggested next". |
| S-2 | `PollGroupEngine.Unsubscribe` blocks up to 5 s per subscription | **STILL OPEN** — `task.Wait(TimeSpan.FromSeconds(5))` at `PollGroupEngine.cs:109`. |
| S-3 | No disposed guard: `Subscribe`/`RegisterAsync` race `DisposeAsync` | **STILL OPEN** — no `_disposed` flag in `PollGroupEngine.cs` or `Core/Hosting/DriverHost.cs`. |
| S-4 | `ScheduledRecycleScheduler` catch-up recycle storm after a stall | **STILL OPEN** — `_nextRecycleUtc += _recycleInterval` (`ScheduledRecycleScheduler.cs:82`), no fast-forward past `utcNow`. |
| S-5 | `GenericDriverNodeManager` re-walk teardown unsynchronized | **STILL OPEN (moot)** — code unchanged; the #10 triage added an OTOPCUA0001 pragma (`GenericDriverNodeManager.cs:69`) explicitly memorializing "zero production references", strengthening the U-1 delete case. |
| P-1 | `Compose` parses each tag's TagConfig JSON four times | **STILL OPEN** — four `JsonDocument.Parse` sites unchanged (`AddressSpaceComposer.cs:541, 606, 638, 670` from `:418–433`). |
| P-2 | Per-operation allocations on the authorization hot path | **STILL OPEN** — `ldapGroups.ToHashSet(...)` per call at `PermissionTrie.cs:42`. |
| P-3 | `PollGroupEngine`: task-per-subscription, no coalescing | **STILL OPEN** — engine unchanged. |
| P-4 | Per-write allocation in the non-idempotent write path | **STILL OPEN** — options record + `Dictionary` per call at `CapabilityInvoker.cs:117–131` (and now production-unreachable — see S-8). |
| C-1 | Byte-parity TagConfig parsing replicated in four projects | **STILL OPEN** — all four copies + "MUST parse identically" comments intact; #13 added a fifth small parity-coupled read (`DeploymentArtifact.TryReadSpec` ResilienceConfig) though that one is a trivial string read. |
| C-2 | Core depends on Configuration; EF entities are the domain model | **STILL OPEN (accepted-risk)** — `Core.csproj:16` still references Configuration. |
| C-3 | `DraftValidator` re-derives NodeId scheme + hard-codes `"GalaxyMxGateway"` | **STILL OPEN** — `DraftValidator.cs:48, 65, 75–97` unchanged. |
| C-4 | Composer violates purity contract with `Trace.TraceWarning` | **STILL OPEN** — `AddressSpaceComposer.cs:493`. |
| C-5 | Inconsistent duplicate-key defensiveness in `Compose` | **STILL OPEN** — last-wins `deviceHostById` (`:357–363`) vs throw-on-dupe `ToDictionary` (`:401–402, 453`). |
| C-6 | Vestigial `Akka` package reference in Commons | **STILL OPEN** — `Commons.csproj:9`. |
| C-7 | Stale scaffold-era XML docs on load-bearing classes | **PARTIALLY FIXED** — fixdocs `3b5ef439` converted `CapabilityInvoker`'s duplicated docs to `<inheritdoc/>`, but both flagged narratives survive verbatim: the applier's F14-scaffold doc ("For now we record the work", "the SDK adapter that lands in F10b will decide…" — `AddressSpaceApplier.cs:7–26`) and `DriverHost`'s "Phase 2 via named-pipe RPC" promise (`Core/Hosting/DriverHost.cs:5–10`). |
| U-1 | Dead/dormant code retained: `GenericDriverNodeManager`, `EquipmentNodeWalker`, `TryParseRelayBody` | **STILL OPEN** — all three present (`Core/OpcUa/` lists both classes + `IdentificationFolderBuilder`; `EquipmentScriptPaths.cs:164`). Evidence grew: the analyzer triage now documents GDNM as test-only scaffolding in a pragma. |
| U-2 | Tier C recycle machinery has no `IDriverSupervisor` implementation | **STILL OPEN** — repo-wide search still finds zero concrete implementations; the new `DriverResilienceOptionsParser` even validates `RecycleIntervalSeconds` as "Tier C only" for a tier that cannot execute (`DriverResilienceOptionsParser.cs:101–110`). |
| U-3 | Continuous-historization initial interest set empty at spawn | **PARTIALLY FIXED / RE-ANALYSED** — the recorder is still spawned with `Array.Empty<string>()` (`Runtime/ServiceCollectionExtensions.cs:277`), but re-analysis shows restart convergence is mechanically present: `DriverHostActor.RestoreApplied` (`DriverHostActor.cs:1293–1320`) fires `RebuildAddressSpace` on bootstrap, `OpcUaPublishActor.HandleRebuild` diffs against an **empty** `_lastApplied` baseline → a full-add plan → `Apply` → `FeedHistorizedRefs` posts the complete set. The prior claim "historizes nothing until the next deploy" was too strong. What remains is exactly CLAUDE.md Known Limitation 2 (reworded by the remediation docs branch): no restart-convergence test and no live end-to-end verification. Kept in the body, downgraded. |
| U-4 | Additive-only ACL model (no Deny) | **STILL OPEN (documented v2.0 gap)** — `PermissionTrie.cs:13–17` unchanged. |
| U-5 | Test coverage strong but unevenly distributed | **PARTIALLY IMPROVED** — new suites landed: `SplitBrainResolverActivationTests` (Cluster.Tests, now 4 files), `DriverCapabilityInvokerFactoryTests` (148 lines), pipeline-builder logging tests, and the Commons `DeferredSinkForwardingReflectionTests` guard. But `ClusterRoleInfo` — the only concurrency-bearing Cluster class — still has zero tests, and the S-1/S-3 surfaces remain untestable because they don't exist. Kept in the body, narrowed. |

Net: **0 of the 5 stability findings, 0 of the 4 performance findings, and 6 of the 7 convention
findings are unchanged**; the remediation wave targeted other domains' Criticals and *landed new
code in this domain* rather than fixing this domain's backlog. That new code is reviewed below
(S-6/S-7/S-8, U-6, C-8).

---

## Architecture Overview

### Composition pipeline (deploy → address space)

The pipeline is a clean three-stage compose/diff/apply design (unchanged this window):

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. The forwarding contract of that
wrapper is now guarded by a reflection-exhaustive test (`Commons.Tests/OpcUa/
DeferredSinkForwardingReflectionTests.cs`) — see C-8 for its one blind spot.

### The new resilience dispatch seam (landed this window)

The Phase 6.1 `CapabilityInvoker`, which the 2026-07-08 review cycle found built-but-never-wired
(RESILIENCE-DISPATCH-GAP), is now live in production dispatch via a Polly-free seam that mirrors
`IDriverFactory`:

- **`Core.Abstractions/IDriverCapabilityInvoker.cs`** — the invoker abstraction (3 methods matching
  `CapabilityInvoker`'s shapes exactly) plus `NullDriverCapabilityInvoker`, a genuine pass-through.
- **`Core.Abstractions/IDriverCapabilityInvokerFactory.cs`** — per-instance factory abstraction plus
  the `Null` factory.
- **`Core/Resilience/DriverCapabilityInvokerFactory.cs`** — the concrete factory: resolves the
  driver-type tier, layers per-instance `ResilienceConfig` JSON on the tier defaults via
  `DriverResilienceOptionsParser.ParseOrDefaults` (logging the parse diagnostic), calls
  `builder.Invalidate(instanceId)` so a respawn with changed options rebuilds cached pipelines, and
  snapshots the options once per `Create` so the per-call accessor is allocation-free.
- The Host's `DriverFactoryBootstrap` registers the factory (`Host/Drivers/DriverFactoryBootstrap.cs:63–70`),
  Runtime resolves it like `IDriverFactory` (`Runtime/ServiceCollectionExtensions.cs:365–366`),
  `DriverHostActor.SpawnChild` creates one invoker per child (`DriverHostActor.cs:1630`), and
  `DriverInstanceActor` wraps its six dispatch sites. `DriverSpawnPlanner` forces a respawn on any
  ResilienceConfig change (`DriverSpawnPlan.cs:40–46`); `DeploymentArtifact.TryReadSpec` reads the
  column null-safely from the artifact (`DeploymentArtifact.cs:168–171`). The OTOPCUA0001 analyzer
  is wired tree-wide and knows the interface as a wrapper home, so an unwrapped capability call is a
  build error.

The layering is genuinely clean — Runtime stays Polly-free, null objects keep every test harness
byte-identical, and the invalidate/respawn pairing closes the options-blind-cache trap. The new
findings below (S-6, S-7, S-8, U-6) are all second-order issues *inside* this new machinery, not
problems with its architecture.

### Supporting Core libraries

- **`Core/Hosting`** — `DriverFactoryRegistry` (driver-type → factory + `GetTier`),
  `DriverFactoryRegistryAdapter`, `DriverHost` (id → `IDriver` lifecycle registry).
- **`Core/Resilience`** — Polly pipelines cached per `(DriverInstanceId, HostName, Capability)`
  (`DriverResiliencePipelineBuilder`, now with retry/breaker event logging as the interim
  observability surface), executed via `CapabilityInvoker` (with the non-idempotent-write no-retry
  override), built per instance by `DriverCapabilityInvokerFactory`, fanned out per host by
  `AlarmSurfaceInvoker`, observed by `DriverResilienceStatusTracker` for Admin `/hosts`.
- **`Core/Stability`** — `MemoryTracking`, `MemoryRecycle` and `ScheduledRecycleScheduler`
  (Tier C recycle via `IDriverSupervisor`), `WedgeDetector`.
- **`Core/Authorization`** — generation-sealed `PermissionTrie` per `(ClusterId, GenerationId)`
  cached in `PermissionTrieCache` (CAS-pruned), walked by `TriePermissionEvaluator` against
  per-session `UserAuthorizationState`.
- **`Commons`** — immutable record message contracts for the Akka DPS topics, strongly-typed ids,
  the deferred sink/publisher pair, `EquipmentScriptPaths`, `OtOpcUaTelemetry`, and now
  `Engines/IScriptCacheOwner` (the apply-boundary compile-cache drop capability for
  `IVirtualTagEvaluator` implementations).
- **`Configuration`** — `OtOpcUaConfigDbContext` (26 entities), the generation-sealed LiteDB
  fallback cache, `ResilientConfigReader`, `DraftValidator`.
- **`Cluster`** — `AddOtOpcUaCluster` / `WithOtOpcUaClusterBootstrap` (now with the extracted,
  testable `BuildClusterOptions` setting `KeepOldestOption { DownIfAlone = true }` explicitly — the
  #11 premise correction is accurately reflected in the akka.conf comment, the XML doc, and
  `SplitBrainResolverActivationTests`), `ClusterRoleInfo`, `ServiceLevelCalculator`, `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** *(unchanged since 2026-07-08)*

`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:148–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. This is the domain's highest-priority open item; STATUS.md already queues it as a
"failure-visibility High".

**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** *(unchanged)*

`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** *(unchanged)*

`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`). 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** *(unchanged)*

`TickAsync` advances `_nextRecycleUtc` by exactly one interval per fire
(`Core/Stability/ScheduledRecycleScheduler.cs:74–82`). 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** *(unchanged; dormancy now formally documented)*

`BuildAddressSpaceAsync` tears down the previous alarm forwarder and clears `_alarmSinks`
non-atomically (`Core/OpcUa/GenericDriverNodeManager.cs:58–73`). Still moot: the #10 analyzer triage
added a pragma at `GenericDriverNodeManager.cs:69` stating outright that this class has "zero
production references" and is exercised only by its own tests — fold this into the U-1 decision.

#### S-6 — NEW: a valid-JSON `ResilienceConfig` with an out-of-range timeout permanently fails every guarded capability call — violates the parser's own never-brick contract — **High**

`DriverResilienceOptionsParser.ParseOrDefaults` defends against *malformed JSON* (`catch
(JsonException)` at `DriverResilienceOptionsParser.cs:71–76`), unknown capability names, and a
misapplied Tier-C recycle interval — but performs **no range validation** on the numeric override
values it merges (`DriverResilienceOptionsParser.cs:91–95`): `TimeoutSeconds`, `RetryCount`, and
`BreakerFailureThreshold` pass straight through. The pipeline builder then does
`builder.AddTimeout(new TimeoutStrategyOptions { Timeout = TimeSpan.FromSeconds(policy.TimeoutSeconds) })`
(`DriverResiliencePipelineBuilder.cs:111–114`). Polly v8 range-validates `TimeoutStrategyOptions.Timeout`
(and `RetryStrategyOptions.MaxRetryAttempts`) at pipeline build time — so `"timeoutSeconds": 0`, a
negative value, or an absurdly large one throws a `ValidationException` inside the
`_pipelines.GetOrAdd` factory on the driver's **first capability call**, and on **every subsequent
call** (a throwing factory caches nothing). Result: subscribe, discover, write, and alarm-ack all
fail persistently for that driver while the driver itself is healthy — precisely the "misconfigured
ResilienceConfig should never brick a working driver" outcome the parser's remarks
(`DriverResilienceOptionsParser.cs:30–33`) promise to prevent.

This is operator-reachable: the AdminUI's `ResilienceFormModel` binds `TimeoutSeconds` as a bare
`int?` with **no range validation anywhere in the form model**
(`AdminUI/Components/Shared/Drivers/ResilienceFormModel.cs:32–37`), so typing `0` into the timeout
field, publishing, and deploying produces the failure. And because #13 correctly forces a respawn on
any ResilienceConfig change, the bad config takes effect immediately.

**Recommendation:** clamp/reject out-of-range values in `ParseOrDefaults` (the diagnostic
out-parameter already exists — `TimeoutSeconds < 1 → tier default + diagnostic`, cap at Polly's max;
`RetryCount < 0 or > 100 → clamp`), add a parser test per field, and mirror the same bounds as
form-side validation in `ResilienceFormModel`. Defense in depth belongs in the parser: the JSON
column is writable by more paths than the AdminUI form.

#### S-7 — NEW: respawn `Invalidate` race can permanently re-cache a stale-options pipeline — **Low**

`DriverCapabilityInvokerFactory.Create` calls `_builder.Invalidate(driverInstanceId)`
(`DriverCapabilityInvokerFactory.cs:71`) before returning the new invoker, and `DriverHostActor`
processes `plan.ToStop` before `plan.ToSpawn` in the same handler (`DriverHostActor.cs:1278–1280`)
— but `StopChild` is an asynchronous actor stop. An in-flight capability call still executing inside
the *old* child's handler can reach `GetOrCreate` **after** the new spawn's `Invalidate` has run,
re-populating the cache with a pipeline built from the **old** options snapshot. Because the builder
deliberately ignores options on a cache hit (`DriverResiliencePipelineBuilder.cs:66–79`), the new
invoker then serves that stale pipeline **indefinitely** — the transient race has a permanent
consequence, silently undoing the operator's ResilienceConfig change for the affected
(host, capability) keys. The window is small (one in-flight handler) but the failure is invisible.

**Recommendation:** make staleness impossible rather than racing it — include an options generation
(or the options reference) in `PipelineKey`, or have `Create` capture a per-invoker generation the
old invoker can no longer write into. Alternatively, defer the new spawn until the old child's
`Terminated` is observed (the actor-name generation counter shows the respawn already tolerates the
old child lingering).

#### S-8 — NEW: production dispatch hardcodes `isIdempotent: true`; the non-idempotent write safeguard is unreachable — **Medium**

The only production call to `ExecuteWriteAsync` is `DriverInstanceActor.HandleWriteAsync` passing
`isIdempotent: true` unconditionally (`Runtime/Drivers/DriverInstanceActor.cs:597–601`); a
repo-wide search finds no caller passing `false`. The `!isIdempotent` no-retry branch in
`CapabilityInvoker.ExecuteWriteAsync` (`CapabilityInvoker.cs:117–131`) — the mechanism Phase 6.1
built specifically so non-idempotent device writes are never retried — is production-dead code. The
inline comment's reasoning ("an OPC UA attribute set is idempotent") holds for plain value writes,
but drivers surface write-through targets where a duplicated write is *not* harmless
(command/pulse coils, increment registers, Galaxy supervisory writes), and the
`WriteIdempotentAttribute` the invoker was designed around is never consulted at the seam. Today
this is behavior-neutral (every tier's Write default is `RetryCount: 0` —
`DriverResilienceOptions.cs:83, 94, 105`), but the entire point of #13 is that an operator can now
switch Write retries on per instance — and the moment one does, **all** of that driver's writes
retry, with the no-retry safety valve unreachable.

**Recommendation:** either thread the tag-level idempotence signal (from `WriteIdempotentAttribute`
/ driver metadata) through `WriteAttribute` to the dispatch site, or — if all-writes-idempotent is
the deliberate v1 posture — delete the `isIdempotent` parameter from the seam and document the
invariant, so the dead branch (and its per-write allocation, P-4) stops implying a guarantee that
isn't wired.

**Positive observations.** The failure-mode engineering in `Configuration/LocalCache` remains
exemplary (fail-closed `GenerationSealedCache`, atomic pointer swaps, the documented LiteDB traps);
`ResilientConfigReader` and `PermissionTrieCache.Prune` are unchanged and correct. The applier's two
historian hooks are properly detached (`AddressSpaceApplier.cs:255–300`). The new remediation code is
itself defensively built: `TryReadSpec` reads `ResilienceConfig` null-safely and the spec survives
absent/null values (`DeploymentArtifact.cs:168–181`); the factory logs parse diagnostics instead of
throwing; `Invalidate` is a correct enumerate-and-`TryRemove` pass over the concurrent dictionary;
`DriverSpawnPlanner`'s respawn-on-ResilienceConfig-change with in-place delta for pure DriverConfig
changes (`DriverSpawnPlan.cs:40–46`) is exactly the right split; and the `DriverInstanceActor`
wrapping preserved the hard-won no-`ConfigureAwait(false)`-on-the-actor-await rule with an explicit
comment that the invoker's internal `ConfigureAwait(false)` does not leak to the caller
(`DriverInstanceActor.cs:290–300`) — backed by a real-invoker actor-context test.

### 2. Performance

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

`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** *(unchanged)*

`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** *(unchanged)*

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.

#### P-4 — Per-write allocation in the non-idempotent write path — **Low** *(unchanged; now also dead code — see S-8)*

`CapabilityInvoker.ExecuteWriteAsync` builds a new options record + `Dictionary` per non-idempotent
write (`Core/Resilience/CapabilityInvoker.cs:117–131`) even though `GetOrCreate` keys only on
`(id, host, capability)` and ignores the options after the first build. Since the only production
caller now passes `isIdempotent: true` (S-8), this path is unreachable — resolve it together with
S-8 (either cache the no-retry snapshot or delete the branch).

**Positive observations.** Pipeline resolution remains lock-free `ConcurrentDictionary` reads; the
new factory snapshots options **once per `Create`** so the per-call accessor is allocation-free —
an explicit hot-path improvement over re-parsing per call. The planner's diff is O(N) dictionary
passes with deterministic sorted outputs; the surgical-apply path and its
whitelist-via-`with`-expression technique (`AddressSpaceApplier.cs:630–663`) are unchanged and
future-field-safe. The new resilience logging only attaches callbacks when a tracker or logger is
present, so unit-test pipelines stay callback-free.

### 3. Conventions

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

`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`

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.

**Recommendation:** move the TagConfig/DeviceConfig intent parsing into `Commons` 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** *(unchanged, accepted-risk)*

`ZB.MOM.WW.OtOpcUa.Core.csproj:16` references `Configuration`; `PermissionTrie` consumes
`Configuration.Enums`, `EquipmentNodeWalker` consumes `Configuration.Entities` directly, and the
composer's plan records are projections of EF entities. A deliberate, consistent choice — flagged
as accepted-risk to document rather than refactor. Notably, the new remediation code got this
boundary **right**: the invoker seam lives in `Core.Abstractions` precisely so Runtime avoids the
Core→Polly closure — new code should keep preferring that pattern.

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

`ValidateNoEquipmentSignalNameCollision` re-implements the folder-scoped NodeId key by hand
(`Configuration/Validation/DraftValidator.cs:75–97`) because `Configuration` cannot reference
`Commons.OpcUa.EquipmentNodeIds` — a third copy of the `{equipment}/{folder}/{name}` scheme.
`ValidateGalaxyTagFullName` hard-codes `dtype != "GalaxyMxGateway"` (`DraftValidator.cs:48`). Both
are symptoms of the same missing shared-contract home as C-1.

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

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** *(unchanged)*

`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:357–363`),
yet `driversById`/`namespacesById`/`scriptsById` use plain `ToDictionary`
(`AddressSpaceComposer.cs:401–402, 453`). Pick one posture; the defensive one is already argued for.

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

`Commons.csproj:9` references the `Akka` package but no Commons source uses an Akka type (the new
`IScriptCacheOwner` doesn't either — only doc comments mention actors). Remove it.

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

The fixdocs sweep (`3b5ef439`) fixed `CapabilityInvoker`'s doc duplication, but both narratives this
finding flagged survive verbatim: `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, and `DriverHost`'s doc still 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 gateways. In a
codebase whose XML documentation is otherwise a genuine asset, stale top-of-class narratives are
actively misleading.

#### C-8 — NEW: the deferred-sink forwarding guard's "exhaustive" interface detection is namespace-filtered — **Low**

`DeferredSinkForwardingReflectionTests.DeferredAddressSpaceSink_implements_exactly_the_known_forwarding_interfaces`
— the guard-of-the-guard whose whole job is catching a *new* capability interface added to the
wrapper — filters `GetInterfaces()` to `i.Namespace == typeof(IOpcUaAddressSpaceSink).Namespace`
(`Commons.Tests/OpcUa/DeferredSinkForwardingReflectionTests.cs:44`), i.e. only
`ZB.MOM.WW.OtOpcUa.Commons.OpcUa`. Commons already has sibling namespaces the wrapper could
plausibly grow an interface from (`Commons.Engines` — home of the brand-new `IScriptCacheOwner` —
and `Commons.Types`): a future forwarding interface declared outside `Commons.OpcUa` would be
silently excluded from `ForwardingInterfaces` enforcement, re-opening exactly the
F10b/PR#423 inert-capability trap class this guard exists to close. The per-method forwarding test
and the `DummyArg` fabricator are otherwise well built (the `DispatchProxy` recorder auto-covers
future members; bool-returning members return `true` so capability-sniffing short-circuits still
record).

**Recommendation:** drop the namespace filter and instead exclude a small explicit allowlist of
known non-forwarding interfaces (e.g. `IDisposable`), so any *unknown* interface trips the guard
regardless of where it lives.

**Positive observations.** Naming and structure remain highly consistent, and the new code follows
the house patterns exactly: the seam mirrors `IDriverFactory`, both null objects are private-ctor
singletons, options records keep `Validate()`-style diagnostics, and every new public member is
documented. Two convention-enforcement mechanisms landed that materially raise the floor: the
OTOPCUA0001 analyzer is wired tree-wide (an unwrapped capability call is now a build error, with
the one remaining pragma carrying a full rationale — `GenericDriverNodeManager.cs:69`), and the
reflection forwarding guard turns the deferred-sink trap from a code-review convention into a
failing test. The #11 premise correction is honest engineering — the SBR comments/docs now state
that the typed option is reinforcing, not the activator, instead of quietly keeping the wrong
war story.

### 4. Underdeveloped Areas

#### U-1 — Dead/dormant code retained in Core: `GenericDriverNodeManager`, `EquipmentNodeWalker`, `TryParseRelayBody` — **Medium** *(unchanged; evidence strengthened)*

- `GenericDriverNodeManager` (`Core/OpcUa/GenericDriverNodeManager.cs`) has no production references
  — now stated *in the source itself* by the #10 triage pragma ("zero production references",
  `GenericDriverNodeManager.cs:69`).
- `EquipmentNodeWalker` (`Core/OpcUa/EquipmentNodeWalker.cs:160–165`) self-declares "retained for
  unit-test support only" — ~280 lines plus `IdentificationFolderBuilder` and a full test suite for
  a path that cannot run in production. Its `ExtractFullName` copy is one of the four C-1 duplicates.
- `EquipmentScriptPaths.TryParseRelayBody` (`Commons/Types/EquipmentScriptPaths.cs:164–172`)
  serviced the deleted relay→alias converter; only tests call it.

Dormant code with green tests is worse than deleted code: it passes every gate while silently
diverging from the live path — the review cycle's own RESILIENCE-DISPATCH-GAP episode proved the
genre. **Recommendation:** delete all three (git preserves them), or move the walker into the test
project if it must survive as a fixture.

#### U-2 — The entire Tier C recycle machinery has no `IDriverSupervisor` implementation — **Medium** *(unchanged; now accreting further)*

`MemoryRecycle`, `ScheduledRecycleScheduler`, and `DriverResilienceStatusTracker.RecordRecycle`
all drive `IDriverSupervisor.RecycleAsync`, but a repo-wide search still finds **zero concrete
implementations**. The gap is now *growing* supporting surface: `DriverResilienceOptionsParser`
carefully gates `RecycleIntervalSeconds` to "Tier C only" (`DriverResilienceOptionsParser.cs:101–110`)
and the AdminUI authors the field — configuration and validation for an execution path that does
not exist. Either document it as speculative infrastructure for a future tier-migration workflow or
prune it alongside U-1.

#### U-3 — Continuous-historization restart convergence: mechanically present, unverified — **Low** *(downgraded from Medium; prior framing partially superseded)*

Re-analysis this cycle shows the prior "after a restart the recorder historizes nothing until the
next deploy" framing was too strong: `DriverHostActor.RestoreApplied` fires a `RebuildAddressSpace`
on bootstrap (`DriverHostActor.cs:1293–1320`), and `OpcUaPublishActor.HandleRebuild` diffs the
restored artifact against an empty `_lastApplied` baseline (`OpcUaPublishActor.cs:290–340`) — a
full-add plan whose `Apply` feeds the **complete** historized set through `FeedHistorizedRefs`
(`AddressSpaceApplier.cs:310–353`). The spawn-time empty set
(`Runtime/ServiceCollectionExtensions.cs:269–278`) is therefore covered by the restore path, and the
recorder is deliberately spawned before the applier wiring so the feed can reach it. What remains is
exactly what CLAUDE.md Known Limitation 2 (as reworded by the remediation) says: **no
restart-convergence test** asserting the recorder re-registers the deployed set after a process
restart, and no live end-to-end verification. Given the subsystem's history of
mechanically-present-but-inert paths, that test is cheap insurance and should be written.

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

`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, but it bounds how fine-grained OT
authorization can get — keep visible in the security docs.

#### U-5 — Test coverage: `ClusterRoleInfo` remains the riskiest untested seam — **Low** *(narrowed from prior)*

This cycle materially improved the domain's test posture: `SplitBrainResolverActivationTests`
(Cluster.Tests), `DriverCapabilityInvokerFactoryTests` (invalidate-per-instance, malformed-config
fallback, override-reaches-execution), pipeline-builder logging tests, and the Commons forwarding
reflection guard all landed. Still missing: `ClusterRoleInfo` — the only concurrency-bearing class
in the Cluster project (lock + subscriber actor + event fan-out) — has **no test** (Cluster.Tests
still covers only pure classes plus the SBR option), and the S-1 applier failure surface and S-3
dispose races remain untested because the surfaces don't exist. An Akka TestKit harness for
`ClusterRoleInfo` leader-change sequencing would close the riskiest untested seam.

#### U-6 — NEW: bulkhead is advertised, authored, parsed — and never applied — **Medium**

The resilience surface documents "retry / circuit-breaker / bulkhead / tracker telemetry" in the
seam docs (`IDriverCapabilityInvoker.cs:4–6`), the analyzer's diagnostic message
(`UnwrappedCapabilityCallAnalyzer.cs:105`), and the tracker's "Polly bulkhead-depth column" doc;
`DriverResilienceOptions` carries `BulkheadMaxConcurrent`/`BulkheadMaxQueue` defaults
(`DriverResilienceOptions.cs:23–25`); the parser merges operator overrides for both
(`DriverResilienceOptionsParser.cs:116–117`); and the AdminUI form authors them
(`ResilienceFormModel.cs:18–20`). But `DriverResiliencePipelineBuilder.Build`
(`DriverResiliencePipelineBuilder.cs:99–177`) adds only timeout → retry → circuit-breaker — there is
no `AddConcurrencyLimiter` (or any rate/concurrency strategy) anywhere in `src/`. The bulkhead is a
dead knob end-to-end: an operator can set it, the deploy respawns the driver, and nothing changes —
the same "configured but inert" genre as the dispatch gap this cycle just fixed, now one layer
deeper.

**Recommendation:** either add the concurrency limiter to the pipeline (keyed like the breaker,
per host) with a factory test proving the queue bound engages, or strip the fields from the
options/parser/form and the word "bulkhead" from the docs until the strategy ships. Don't leave the
knob authored-but-unwired — this codebase's history says it will be trusted.

---

## Maturity Ratings

| Dimension | 2026-07-08 | 2026-07-12 | Justification |
|---|---|---|---|
| Stability | **4** | **4** | The deliberate failure-mode engineering (fail-closed caches, detached hooks, null-object seams) still defines the domain, and the new dispatch wiring was built carefully (respawn-on-change, invalidate pairing, actor-context preservation). But nothing on the stability backlog moved — S-1's swallow-everything deploy reporting remains the headline — and the new machinery introduced a genuine operator-reachable bricking vector (S-6) that directly contradicts its own never-brick contract. |
| Performance | **3** | **3** | All four performance findings are untouched (4× JSON parse, per-node authorization garbage, task-per-subscription engine, per-write allocation). The new code is hot-path conscious (once-per-Create options snapshot, callback-free pipelines without observers) but adds no throughput improvements to the existing paths. |
| Conventions | **4** | **4** | Already exceptionally consistent; this window added *mechanical enforcement* (tree-wide OTOPCUA0001 as a build error, the reflection forwarding guard) which is the right direction — but the systemic byte-parity duplication (C-1/C-3) is untouched, the two flagged stale class narratives survived a dedicated fixdocs pass (C-7), and the new guard itself has a namespace blind spot (C-8). |
| Underdeveloped areas | **3** | **3** | The cycle's biggest dormancy (CapabilityInvoker never dispatched) is genuinely fixed and analyzer-guarded, and U-3 turned out mostly mechanically closed. But U-1's three dead code paths and U-2's implementation-less Tier C machinery are untouched — the latter now *accreting* config/validation surface — and a new authored-but-inert knob shipped (U-6 bulkhead). The pattern the review flagged (built, tested, unreachable) is still being produced. |
