# 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//` 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 ``` 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).