Files
lmxopcua/archreview/07-client-tooling-engineering.md
T
Joseph Doherty 9fadead6a6 docs(archreview): remediation plans + fix flagged doc drift
Add the 7 per-domain design+implementation plans (archreview/plans/) with
an index, produced from the 2026-07-08 architecture review.

Fix two confirmed doc drifts the review flagged (theme #5):
- CLAUDE.md KNOWN LIMITATION 2: the continuous-historization historized-ref
  feed IS wired (AddressSpaceApplier.FeedHistorizedRefs -> UpdateHistorizedRefs
  -> recorder); rewrite to reflect that value-capture is code-complete and only
  the live end-to-end + restart-convergence verification remains.
- CLAUDE.md ScriptAnalysis gating: endpoints use Roles=Administrator,Designer
  via RequireAuthorization, not the FleetAdmin policy.
2026-07-08 16:14:37 -04:00

28 KiB
Raw Blame History

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:37MODBUS_SIM_ENDPOINT default 10.100.0.35:5020), and every test Assert.Skips 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.TestsSubscribeCommandTests.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'):

<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).