# Architecture Review 07 — Client Tooling, Analyzers, and the Cross-Cutting Engineering System

| | |
|---|---|
| **Date** | 2026-07-08 |
| **Commit** | `9cad9ed0` (master) |
| **Reviewer** | Architecture review agent (deep-review sweep, slice 07) |
| **Scope** | `src/Client/*` (Client.CLI, Client.Shared, Client.UI) + `tests/Client/*`; `src/Tooling/ZB.MOM.WW.OtOpcUa.Analyzers` + `tests/Tooling/*`; cross-cutting: `Directory.Build.props`, `Directory.Packages.props`, `NuGet.config`, `ZB.MOM.WW.OtOpcUa.slnx`, `.github/workflows/`, `ci/`, `scripts/`, `docker-dev/`, overall test architecture, docs freshness, repo-root hygiene |

---

## Architecture Overview

### Client stack

Three projects form a clean layered client stack, all .NET 10:

- **`Client.Shared`** — the OPC UA client library. `OpcUaClientService`
  (`src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/OpcUaClientService.cs`, 975 lines) implements
  `IOpcUaClientService` behind five adapter seams (`IApplicationConfigurationFactory`,
  `IEndpointDiscovery`, `ISessionFactory`, `ISessionAdapter`, `ISubscriptionAdapter` under
  `Adapters/`) so the OPC Foundation SDK is fully fakeable — the test project supplies
  `Fakes/Fake*` for every seam. The service owns connect/disconnect, read/write with
  string→typed value coercion, browse with continuation points, data + alarm subscriptions
  with **failover replay** (multi-endpoint round-robin on keep-alive failure), Part 9 alarm
  method calls (Acknowledge/Confirm/Shelve/Enable/Disable), HistoryRead (raw + aggregate),
  and redundancy-info reads.
- **`Client.CLI`** — CliFx-based terminal client (`otopcua-cli`). 14 commands
  (`Commands/`: connect, read, write, browse, subscribe, historyread, alarms, redundancy,
  acknowledge, confirm, shelve, enable, disable + base). `CommandBase` centralises
  connection options; `Program.cs` is a 14-line CliFx bootstrap with a type-activator that
  injects a shared `IOpcUaClientServiceFactory`. Commands are session-per-invocation
  (create → connect → work → disconnect/dispose in `finally`).
- **`Client.UI`** — Avalonia 11.2 desktop app (CommunityToolkit.Mvvm). **Real, not
  vestigial**: ~2,905 lines of C# + 551 lines of AXAML across 10 viewmodels, 9 views, a
  custom `DateTimeRangePicker` control, JSON settings persistence, and a UI-dispatcher
  seam (`IUiDispatcher` with a synchronous test double). Covers browse tree, read/write,
  subscriptions, alarms (ack/confirm/shelve dialogs), and history. 126 headless
  (Avalonia.Headless) tests.

### Tooling

`src/Tooling/ZB.MOM.WW.OtOpcUa.Analyzers` ships exactly one Roslyn analyzer:
**OTOPCUA0001 `UnwrappedCapabilityCallAnalyzer`** — flags async calls to the seven guarded
driver-capability interfaces (`IReadable`, `IWritable`, `ITagDiscovery`, `ISubscribable`,
`IHostConnectivityProbe`, `IAlarmSource`, `IHistoryProvider`) that are not wrapped in
`CapabilityInvoker.ExecuteAsync/ExecuteWriteAsync` (the Polly breaker/retry/bulkhead
pipeline). Semantically sophisticated (symbol-identity matching, DIM handling,
wrapper-lambda containment), netstandard2.0, `EnforceExtendedAnalyzerRules`, tracked
`AnalyzerReleases.*.md`, 31 tests. **But it is wired into zero consuming projects** — see
finding C-1.

### Test architecture map

47 test projects mirror `src/` under `tests/<module>/` with three tiers:

1. **`*.Tests`** — pure unit suites (fakes/in-memory), 40 projects. xunit.v3 + Shouldly
   everywhere except three legacy v2 holdouts still on xunit 2.9.2 (`AdminUI.Tests`,
   `ControlPlane.Tests`, `Runtime.Tests`).
2. **`*.IntegrationTests`** — 10 projects needing a live fixture. Pattern: a collection
   fixture does a **one-shot TCP reachability probe** against an env-var endpoint whose
   default is the shared Docker host `10.100.0.35` (e.g.
   `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusSimulatorFixture.cs:37`
   — `MODBUS_SIM_ENDPOINT` default `10.100.0.35:5020`), and every test `Assert.Skip`s when
   unreachable. `dotnet test` therefore passes cleanly offline — by *skipping*.
3. **`Category=LiveIntegration`** — env-gated live suites against real infrastructure
   (`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Live/GatewayLiveIntegrationTests.cs`,
   gated on `HISTGW_GATEWAY_ENDPOINT` etc.). Skip-clean when env vars absent.
4. **`Category=E2E`** — reserved; **no project or test carries it yet** (the nightly E2E
   workflow is a documented no-op).

### Build / CI pipeline shape

- `Directory.Build.props` — global net10.0 / nullable / implicit usings / latest lang, plus
  the CVE-2025-6965 `NuGetAuditSuppress` carve-out. `TreatWarningsAsErrors` is deliberately
  **opt-in per project** (legacy xUnit1051 debt), with a written promotion plan.
- `Directory.Packages.props` — central package management, all 100+ versions pinned, no
  floating versions, three inline security-pin rationales (Roslyn 5.0.0 CS9057 pin,
  OpenTelemetry 1.15.3, Tmds.DBus.Protocol 0.21.3).
- `NuGet.config` — three sources with **strict packageSourceMapping** (nuget.org wildcard,
  repo-local `nuget-packages/` for MxGateway, Gitea feed for the ZB.MOM.WW.* shared libs).
- CI: two workflow files in `.github/workflows/` (remote is Gitea; header claims
  Gitea-Actions compatibility). `v2-ci.yml`: build → 5-project unit matrix → 2-project
  integration matrix. `v2-e2e.yml`: nightly docker-dev fleet + `Category=E2E` filter that
  matches zero tests.
- `ci/` contains only `ab-server.lock.json` (pinned libplctag release for the AB fixture).
- `scripts/` — PowerShell operational tooling: `compliance/` (6 phase-gate scripts), `e2e/`
  (per-driver E2E harnesses + sample config), `install/` (Windows service + Traefik),
  `migration/`, `smoke/` (SQL seeds), `focas/` (protocol capture), and
  `check-code-reviews-readme.ps1` (review-index consistency check — not run by CI).
- `docker-dev/` — the local 8-service dev fleet (SQL, migrator, cluster-seed, central-1/2,
  site-a/b pairs, Traefik).

---

## Findings

Severity scale: **Critical** (broken now, corrupts trust) / **High** (material gap, fix soon)
/ **Medium** (real debt) / **Low** (polish).

### 1. STABILITY

#### S-1 (High) — CI gates ~15% of the test matrix; client, tooling, driver, and most core suites are never run by CI
`.github/workflows/v2-ci.yml:47-52` enumerates **5** unit-test projects (Cluster,
ControlPlane, Runtime, Security, OpcUaServer) out of **47** in the solution. Everything in
this review's scope — `Client.CLI.Tests` (104 tests), `Client.Shared.Tests` (158),
`Client.UI.Tests` (126), `Analyzers.Tests` (31) — plus all driver unit suites, driver CLI
suites, and most Core suites (Core, Core.Scripting, VirtualTags, ScriptedAlarms,
AlarmHistorian, Commons, Configuration, Abstractions) run only when a developer remembers
to run them. The matrix predates most of these projects and was never widened.
**Recommendation:** replace the hand-maintained matrix with a single
`dotnet test ZB.MOM.WW.OtOpcUa.slnx --filter "Category!=E2E&Category!=LiveIntegration"`
leg (the skip-gated integration fixtures already tolerate missing endpoints), or generate
the matrix from the slnx. Any new-project drift then becomes impossible.

#### S-2 (High) — "green CI" for integration tests means "skipped", and nothing distinguishes skip from pass
The integration job (`v2-ci.yml:61-76`) runs `Host.IntegrationTests` and
`OpcUaServer.IntegrationTests` on `ubuntu-latest` with **no service containers**, while
the fixtures default to `10.100.0.35` (unreachable from any hosted runner —
`ModbusSimulatorFixture.cs:37`, `AbServerFixture.cs`, `Snap7ServerFixture.cs`,
`OpcPlcFixture.cs`, and `Host.IntegrationTests/DriverTestConnectE2eTests.cs` all
hard-default to it). Probe-fails → `Assert.Skip` → job green. The design is intentional
and well-documented for dev boxes, but in CI it silently converts the entire integration
tier into a no-op with a passing badge. **Recommendation:** in CI, either start the
fixtures as workflow `services:` (modbus/opc-plc images exist) and set the `*_ENDPOINT`
env vars to `localhost`, or add a post-test step that fails the job when skipped-count >
threshold (`--logger trx` + parse), so a silent fixture outage cannot masquerade as green.

#### S-3 (Medium) — the nightly E2E workflow is a permanent green no-op
`v2-e2e.yml:6-10` says it plainly: the E2E test project "does not yet exist… this workflow
is a green no-op". No test in the tree carries `Category=E2E` (verified by grep). A
nightly job that always passes trains people to ignore it; it also boots the full
docker-dev fleet for nothing. **Recommendation:** either land the minimal E2E round-trip
project (the `scripts/e2e/test-*.ps1` harnesses show exactly what it should assert) or
disable the schedule until it exists.

#### S-4 (Medium) — fixed-sleep timing tests are the dominant wait style
`grep Task.Delay|Thread.Sleep` across `tests/` (excluding obj) shows heavy fixed-delay
usage: Driver.Galaxy.Tests (30), Driver.Modbus.Tests (22), Driver.AbCip.Tests (20),
Host.IntegrationTests (14), and in this slice **Client.CLI.Tests** —
`SubscribeCommandTests.cs:30,55,78,103`, `AlarmsCommandTests.cs:26,51,76,104,127,149`
(`await Task.Delay(100)` to let a background command loop start before cancelling),
`EventHandlerLifecycleTests.cs:54` (150 ms). These pass locally and flake under CI load —
which is currently masked because CI never runs them (S-1). **Recommendation:** replace
start-up sleeps with a readiness signal from the fake service (e.g. a
`TaskCompletionSource` completed on first `SubscribeAsync`), which `FakeOpcUaClientService`
can expose cheaply.

#### S-5 (Medium) — Client.Shared subscription bookkeeping has lock-discipline gaps
`OpcUaClientService.cs` documents that `_subscriptionLock` guards the subscription state
(lines 18-22), but:
- `SubscribeAsync` checks/creates the shared `_dataSubscription` **outside** the lock
  (line 262) — two concurrent first-subscribers can both see null and create two SDK
  subscriptions, leaking one.
- `RunFailoverAsync` nulls `_dataSubscription` / `_alarmSubscription` **without** the lock
  (lines 695-696), while `DisconnectAsync`'s comment (lines 138-140) assumes the failover
  path nulls them under the lock.
- `UnsubscribeAlarmsAsync` (line 335) does not take `_alarmSubscribeSemaphore`, so an
  unsubscribe racing a `SubscribeAlarmsAsync` can delete the adapter the subscriber just
  created and leave `_alarmSubscription` non-null-but-deleted.
- Keep-alive failover is fire-and-forget (`_ = HandleKeepAliveFailureAsync()`, lines
  115, 718) — correct re-entrancy guard via `Interlocked.CompareExchange` (line 659), but
  an exception escaping `TransitionState`'s event invocation is unobserved.

For the CLI's session-per-command usage these races are near-unhittable; for Client.UI
(long-lived service, UI-thread + keep-alive-thread concurrency) they are real.
**Recommendation:** move the `_dataSubscription` null-check/create inside a small async
gate (mirror `_alarmSubscribeSemaphore`), take the lock in `RunFailoverAsync`, and route
`UnsubscribeAlarmsAsync` through the semaphore.

#### S-6 (Medium) — global `Log.Logger` swap per CLI command
`CommandBase.ConfigureLogging()` (`CommandBase.cs:120-134`) does `Log.CloseAndFlush()` then
replaces the static `Log.Logger` on every command execution. In-process this is a race for
any parallel test collections that execute commands concurrently (xunit.v3 parallelises
collections by default), and it makes the CLI hostile to embedding. The
`LoggerLifecycleTests` suite exists precisely to police this. **Recommendation:** give
each command an instance `ILogger` (Serilog `LoggerConfiguration.CreateLogger()` held per
execution) instead of mutating the global.

#### S-7 (Low) — no `global.json`, but the build depends on an exact SDK band
`v2-ci.yml:10-12` claims "The .NET 10 SDK is pinned via global.json at the repo root" —
**no `global.json` exists**. Meanwhile `Directory.Packages.props:44-49` pins
`Microsoft.CodeAnalysis.CSharp` to 5.0.0 explicitly because "SDK 10.0.105 ships compiler
5.0.0.0… until the SDK rolls to 10.0.110+". An SDK roll on a runner or dev box silently
changes the compiler this pin was matched to. **Recommendation:** add `global.json` with
`rollForward: latestFeature` and revisit the Roslyn pin note when it lands.

#### S-8 (Low) — first-caller interval wins for the shared data subscription
`SubscribeAsync` creates one `_dataSubscription` with the first caller's `intervalMs`
(`OpcUaClientService.cs:262`); later subscriptions with different intervals join the same
publish interval silently (the monitored-item sampling interval is honoured, the publish
cadence is not). Fine for the CLI; surprising for UI users mixing 100 ms and 10 s items.
Document or create per-interval subscriptions.

### 2. PERFORMANCE

#### P-1 (Medium) — CI does 7 independent restore+build passes per push with no caching
Every matrix leg in `v2-ci.yml` checks out and implicitly restores/builds from scratch
(`dotnet test` without `--no-build`), and the dedicated `build` job's outputs are thrown
away. No `actions/cache` for the NuGet package folder. For a solution this size (70+
projects) that is the bulk of CI wall-clock. **Recommendation:** cache
`~/.nuget/packages` keyed on `Directory.Packages.props`, and either share the build via
artifacts or accept rebuild but add `--no-restore` after an explicit cached restore.

#### P-2 (Low) — recursive browse is N+1 over `HasChildrenAsync`
`OpcUaClientService.BrowseAsync` issues an extra `HasChildrenAsync` round-trip per Object
child (`OpcUaClientService.cs:230-231`), and `SubscribeCommand.CollectVariablesAsync`
walks the tree serially (`SubscribeCommand.cs:256-281`). On the fleet address space
(thousands of equipment folders) `subscribe -r` start-up is dominated by this. Acceptable
for a diagnostic tool; batch the browse (`BrowseNext` on multiple nodes / read
`References` in bulk) if it becomes a soak-test bottleneck.

#### P-3 (Low, positive) — integration-fixture cost is well-engineered
The probe-once collection-fixture pattern (`ModbusSimulatorFixture` remarks: "checking
every test would waste several seconds against a firewalled endpoint") is consistently
applied across all 10 IntegrationTests projects, and fixtures do not hold sockets open.
No per-test container spin-up anywhere. This is the right shape; the problem is what CI
does with it (S-2), not the fixtures themselves.

#### P-4 (Low) — CLI subscription output path is allocation-sane
`SubscribeCommand` serialises SDK callbacks through an unbounded single-reader `Channel`
(`SubscribeCommand.cs:118-119`) rather than locking the console writer — correct and
cheap. The unbounded channel could balloon if the console blocks while thousands of
monitored items update; a bounded channel with `DropOldest` would cap it. Cosmetic at
current scale.

### 3. CONVENTIONS

#### C-1 (High) — the custom analyzer is wired into **zero** projects; OTOPCUA0001 enforces nothing
A repo-wide grep of every `.csproj`, `.props`, and `.targets` finds only two references to
`ZB.MOM.WW.OtOpcUa.Analyzers`: its own csproj and its test project's `ProjectReference`.
No consuming project references it with `OutputItemType="Analyzer"`, and
`Directory.Build.props` does not inject it. The carefully built rule — "every
IReadable/IWritable/… call must route through CapabilityInvoker" — is enforced **only by
its 31 unit tests asserting the analyzer itself works**, not against the actual Core/
Server/Driver code it was written to police. This is the same failure mode the project
has already been bitten by twice at runtime (the dormant `GatewayTagProvisioner`, the
non-forwarding `DeferredAddressSpaceSink`): a capability built and tested but never
plugged in. **Recommendation:** add to `Directory.Build.props` (conditioned on
`'$(MSBuildProjectName)' != 'ZB.MOM.WW.OtOpcUa.Analyzers'`):
```xml
<ItemGroup>
  <ProjectReference Include="$(MSBuildThisFileDirectory)src/Tooling/ZB.MOM.WW.OtOpcUa.Analyzers/ZB.MOM.WW.OtOpcUa.Analyzers.csproj"
                    OutputItemType="Analyzer" ReferenceOutputAssembly="false" PrivateAssets="all" />
</ItemGroup>
```
then triage the first wave of OTOPCUA0001 warnings (add the documented test-project
`NoWarn` where intended).

#### C-2 (Medium) — `TreatWarningsAsErrors` opt-in never reached the Client/Tooling slice (or most tests)
`Directory.Build.props:2-11` explains TWE is per-project pending legacy cleanup, and every
Core, Server, Driver, and Driver-CLI **src** project has opted in — but
`Client.CLI.csproj`, `Client.Shared.csproj`, `Client.UI.csproj`, and
`Analyzers.csproj` have not, and only 9 of 47 test projects have. The comment's stated
blocker ("pre-v2 test projects… xUnit1051") does not apply to the client stack, which is
v2-era code. **Recommendation:** add TWE to the four remaining src projects now (they
build warning-clean or nearly so), and burn down the test-project debt per module.

#### C-3 (Low) — central package management discipline is exemplary; the audit carve-out is still valid
Verified current state: CPM enabled, every version pinned, `packageSourceMapping` prevents
dependency-confusion for the private `ZB.MOM.WW.*` namespaces (`NuGet.config:9-29`), and
the `NuGetAuditSuppress` for GHSA-2m69-gcr7-jv3q (`Directory.Build.props:19-32`) is still
present, still scoped to a single advisory, and still accurate as written (transitive
SQLitePCLRaw native bundle, no patched release; documented removal condition). The two
transitive CVE pins (OpenTelemetry 1.15.3 at `Directory.Packages.props:80-87`,
Tmds.DBus.Protocol 0.21.3 at lines 104-107 with the matching direct reference in
`Client.UI.csproj`) follow the memory-documented "surgical direct reference" strategy.
No action; keep the removal reminders alive.

#### C-4 (Low) — test naming/layout is consistent; one categorisation deviation
`*.Tests` vs `*.IntegrationTests` naming is uniform and mirrors `src/` exactly. The single
deviation: live-gated tests live *inside* a unit-suite project
(`Driver.Historian.Gateway.Tests/Live/GatewayLiveIntegrationTests.cs`,
`Category=LiveIntegration`) rather than a `*.IntegrationTests` sibling — harmless because
they skip-gate on env vars, but a `dotnet test tests/Drivers/...Gateway.Tests` run now has
a hidden live-test dependency surface. Framework split: 44 projects on xunit.v3, 3 legacy
on xunit 2.x (`AdminUI.Tests`, `ControlPlane.Tests`, `Runtime.Tests`) — finish the
migration and drop the `xunit` 2.9.2 pin from `Directory.Packages.props:108`.

#### C-5 (Low) — no mechanical code-style enforcement exists; StyleGuide.md is a *docs* style guide
There is **no `.editorconfig`** in the repo, no `dotnet format` CI step, and
`StyleGuide.md` is exclusively a documentation-writing guide — whose opening line still
says "for all **ScadaBridge** documentation" (`StyleGuide.md:3`), a copy-paste from the
sister repo. Code style is therefore enforced only by review culture. **Recommendation:**
add a root `.editorconfig` (the implicit conventions are already consistent — file-scoped
namespaces, 4-space, `_camelCase` fields) and fix the StyleGuide title/product name.

#### C-6 (Low) — csproj boilerplate duplicates Directory.Build.props
Nearly every csproj restates `TargetFramework`/`Nullable`/`ImplicitUsings` that
`Directory.Build.props:12-17` already supplies (e.g. `Client.CLI.csproj:5-8`). Harmless
but it means an SDK bump requires touching 70+ files instead of one. Strip on next sweep.

### 4. UNDERDEVELOPED AREAS

#### U-1 — Client.UI maturity: real product, adequately tested, current docs
Verdict: **not vestigial**. ~2.9 k LOC C#, 551 lines AXAML, MVVM with dispatcher and
settings seams, alarm ack/confirm/shelve dialogs, history view with custom range picker,
126 headless tests, and `docs/Client.UI.md` matches the code (verified stack table and
window-layout claims). It is the least exercised layer in anger (no CI — S-1 — and no
E2E), and headless tests can't catch AXAML binding regressions (the AdminUI "no bUnit —
live-verify" lesson applies equally here). Rating it "maturing", not "underdeveloped".

#### U-2 (Medium) — source → test coverage matrix: three structural gaps
Matrix over the 41 solution src projects (test projects verified against the slnx):

| Source project | Unit tests | Integration | Gap |
|---|---|---|---|
| Client.Shared | ✅ Client.Shared.Tests (158) | — | none |
| Client.CLI | ✅ Client.CLI.Tests (104) | — | none |
| Client.UI | ✅ Client.UI.Tests (126, headless) | — | none |
| Tooling/Analyzers | ✅ Analyzers.Tests (31) | — | **not wired into builds (C-1)** |
| Core, Commons, Cluster, Configuration, Core.Scripting, VirtualTags, ScriptedAlarms, AlarmHistorian, Core.Abstractions | ✅ each | — | none |
| **Core.Scripting.Abstractions** | ❌ | — | no dedicated tests (thin interfaces; covered incidentally by Core.Scripting.Tests) |
| AdminUI, ControlPlane, Runtime, Security, OpcUaServer | ✅ each | OpcUaServer.IntegrationTests | none |
| **Host** | ❌ **no Host.Tests** | Host.IntegrationTests only | Host's DI/wiring logic (the layer where both "dormant wiring" bugs lived) has no unit tier |
| All 8 protocol/gateway drivers + Modbus.Addressing + 2 Browsers + Historian.Gateway | ✅ each | 7 × IntegrationTests + LiveIntegration | none |
| **8 × `*.Contracts` projects** (Galaxy, Modbus, S7, AbCip, AbLegacy, TwinCAT, FOCAS, OpcUaClient) | ❌ | — | DTO-only; consuming-suite coverage; OpcUaClient.Contracts NamespaceMap gap already known-deferred |
| 7 × Driver CLIs + Cli.Common | ✅ each | — | none |

The one gap worth acting on is **Host**: it is where registration/forwarding mistakes
land, and its only automated coverage requires the 2-node harness. A `Host.Tests` project
asserting DI composition (e.g. "when `ServerHistorian:Enabled`, `IHistorianProvisioning`
resolves to `GatewayTagProvisioner` and the applier receives it") would have caught the
PR #423 dormancy at unit speed.

#### U-3 (Medium) — docs drift: `docs/Client.CLI.md` documents 8 of 14 commands
Spot-checks performed:
1. `docs/Client.CLI.md` command sections: connect, read, write, browse, subscribe,
   historyread, alarms, redundancy — **`ack`, `confirm`, `shelve`, `enable`, `disable`
   have zero mentions** (grep), yet the commands ship
   (`Commands/AcknowledgeCommand.cs`, `ConfirmCommand.cs`, `ShelveCommand.cs`,
   `EnableCommand.cs`, `DisableCommand.cs`) and CLAUDE.md explicitly says "Client.CLI
   supports `ack`, `confirm`, `shelve` commands. See `docs/Client.CLI.md` for full
   documentation." Five commands undocumented.
2. The doc's "104 unit tests" claim (`docs/Client.CLI.md`, Testing section) — **accurate**:
   exactly 104 `[Fact]`/`[Theory]` in Client.CLI.Tests today.
3. `docs/Client.UI.md` stack/layout claims — **accurate** vs csproj and Views/.
4. `v2-ci.yml:10-12` global.json claim — **false** (S-7).
Recommendation: add the five missing command sections; they are the operator-facing alarm
workflow.

#### U-4 (Medium) — repo-root planning-file sprawl contradicts its own rules
Five point-in-time planning/state files are committed at the root: `looseends.md` (state
as of 2026-05-18), `pending.md` (2026-06-16), `stillpending.md`, `current.md`,
`HISTORIAN-GATEWAY-INTEGRATION-ISSUES.md`. `pending.md` itself declares "HARD RULE: never
`git add .`; never stage `pending.md` / `current.md` / …" — **yet `pending.md` and
`current.md` are tracked** (they appear in `git ls-files`; last commits `cd20c3c0`,
`384dbd7d`). The memory index says the stillpending backlog is ~95% shipped, so most of
this content is stale snapshots that now contradict `docs/` and CLAUDE.md.
**Recommendation:** either move them under `docs/plans/` with dated names (the existing
convention) or delete + gitignore them; make the file's own rule true.

#### U-5 (Medium) — orphaned proprietary AVEVA DLLs tracked in `lib/`
`lib/` contains 7 **committed** vendor binaries (`aahClient.dll`, `aahClientCommon.dll`,
`aahClientManaged.dll`, `ArchestrA.MXAccess.dll`, `ArchestrA.CloudHistorian.Contract.dll`,
`Historian.CBE.dll`, `Historian.DPAPI.dll`). No csproj anywhere references them (zero
`HintPath` hits repo-wide) — they are leftovers from the retired Wonderware sidecar /
in-process MXAccess era. Committed proprietary SDK DLLs are a redistribution/licence risk
in any clone of this repo and dead weight in history. **Recommendation:** `git rm -r lib/`
(the bitness/COM story now lives entirely in the mxaccessgw repo, per CLAUDE.md).

#### U-6 (Low) — secrets hygiene: `sql_login.txt` is NOT committed (gitignored), but is still a plaintext credential at the root
Verified: `.gitignore:47` lists it, `git check-ignore` confirms, and
`git log -- sql_login.txt` is empty — the file has never been committed. The remaining
risk is purely local (plaintext `wwadmin` password for `wonder-sql-vd03` sitting in a
Desktop directory that agents and sync tools read). **Recommendation:** move to
`dotnet user-secrets` / an env file outside the repo; at minimum keep it out of any
export bundle (`export-clean-copy.bat` at the root should be checked to exclude it).
Also note the retired `Driver.Historian.Wonderware*` project dirs still exist on disk as
ignored `bin/obj` husks — local-only debris, safe to delete.

#### U-7 (Low) — missing CI stages (inventory)
Not gated anywhere today: analyzer tests + analyzer enforcement (C-1), all client/driver/
core unit suites (S-1), `dotnet format`/style (C-5), NuGet vulnerability audit as an
explicit failing step (currently only implicit in restore warnings, which are not TWE'd
in most projects), the `scripts/check-code-reviews-readme.ps1` consistency check, and any
docs link/freshness check. The `ci/ab-server.lock.json` fixture pin references a
"GitHub Actions step" in `docs/v2/test-data-sources.md` that does not exist in either
workflow — the AB fixture download step was planned but never landed.

#### U-8 (Low) — CLI security posture is dev-tool-grade by construction
`CommandBase.CreateConnectionSettings()` hardcodes `AutoAcceptCertificates = true`
(`CommandBase.cs:91`), so `--security signandencrypt` encrypts but never authenticates the
server (any cert accepted → MITM-able), and `-P` takes the password as a process-visible
argv. Acceptable for a diagnostic tool, but the doc should say so, and a
`--strict-certs` opt-in would be cheap. Also `Client.Shared`'s alarm fallback path bakes
Galaxy-specific attribute names (`.InAlarm`, `.Acked`, `.TimeAlarmOn`, `.DescAttrName`)
into the generic client library (`OpcUaClientService.cs:851-871`) — a documented but
layering-violating convenience.

---

## Maturity Ratings (1 = ad hoc, 5 = exemplary)

| Dimension | Rating | Justification |
|---|---|---|
| Stability | **2.5** | The client code and fixture patterns are careful, but CI runs ~15% of the suite, the integration tier silently skips to green on hosted runners, and the nightly E2E is a documented no-op — the safety net exists mostly on developer machines. |
| Performance | **3.5** | Build/test structure is lean (CPM, probe-once fixtures, channel-serialised CLI output); loses points for 7× uncached restore/builds in CI and the N+1 recursive browse. |
| Conventions | **3** | Package management and test layout are exemplary and self-documenting, but the flagship convention-enforcement tool (the analyzer) is wired into nothing, TWE never reached the client slice, and there is no .editorconfig — conventions hold by culture, not mechanism. |
| Underdeveloped areas | **3** | Client.UI is genuinely mature and the coverage matrix is nearly complete (Host is the one real hole); dragged down by 5-command docs drift, committed stale planning files that violate their own rules, and orphaned proprietary DLLs in `lib/`. |

---

## Top recommendations (ordered)

1. Wire `ZB.MOM.WW.OtOpcUa.Analyzers` into every project via `Directory.Build.props` (C-1).
2. Widen `v2-ci.yml` to the whole solution and make skipped integration tests visible/failing in CI (S-1, S-2).
3. Delete tracked `lib/` vendor DLLs (U-5) and resolve the root planning-file contradiction (U-4).
4. Add the five missing command sections to `docs/Client.CLI.md` (U-3).
5. Add `global.json` (S-7), `Host.Tests` (U-2), and TWE to the four client/tooling csprojs (C-2).
6. Fix the three `OpcUaClientService` lock-discipline gaps before Client.UI grows long-lived multi-thread usage (S-5).
