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

412 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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).