Files
mxaccessgw/archreview/50-clients.md
T
Joseph Doherty 59856b8c63 docs(archreview): add architecture review + per-domain remediation designs and tracker
Adds the 2026-07-08 architecture review (00-overall + six domain reports)
and a remediation/ tree: one design+implementation doc per domain covering
every finding, plus 00-tracking.md as the master progress tracker.

- 153 findings with stable IDs (GWC/WRK/IPC/SEC/CLI/TST), each with
  design rationale, implementation steps, tests, docs, and verification.
- Tracker rolls findings up by severity and P0/P1/P2 roadmap tier, records
  cross-cutting clusters and per-finding status (all Not started).
- Planning docs only; no source changes.
2026-07-09 00:39:00 -04:00

148 lines
26 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.
# Language Clients — Architecture Review
## Scope & method
This review covers the five official client libraries and their CLIs: `clients/dotnet`, `clients/go`, `clients/java`, `clients/python`, `clients/rust`, plus `clients/proto` and the client-facing docs (`docs/ClientLibrariesDesign.md`, `docs/ClientPackaging.md`, `docs/ClientBehaviorFixtures.md`, `docs/CrossLanguageSmokeMatrix.md`). All clients consume the shared protos in `src/ZB.MOM.WW.MxGateway.Contracts/Protos`. The review is static (macOS tree, no builds run): every handwritten source file in each client library was read, CLIs were spot-checked, generated directories were verified to exist and were excluded from style review. All findings cite `path:line` in the repository root. The Java client was reviewed specifically for JDK 17 compatibility following the `feat/jdk17-client-retarget` work.
## Executive summary
- All five clients implement the same core shape — connect, open/close session, typed Register/AddItem/Advise/Write helpers, raw `Invoke` escape hatch, event streaming with a resume cursor, alarm RPCs, and Galaxy browse with a lazy walker — and all five load the shared behavior fixtures in tests. Overall maturity is high and unusually uniform for a five-language surface.
- The Java JDK 21→17 retarget is correct in build config (`toolchain 17` + `options.release = 17`) and no JDK 21+ APIs remain in source; however three docs still say Java 21 (`clients/java/README.md:354`, `clients/java/JavaClientDesign.md:34`, `docs/ClientPackaging.md:193`), violating the repo's docs-in-same-commit rule.
- The published Rust crate cannot build outside this repository: `build.rs` resolves protos at `../../src/ZB.MOM.WW.MxGateway.Contracts/Protos` and packaging runs `cargo package/publish --no-verify`, so the defect is never caught. This is the most serious packaging finding.
- The Go `Session.Events()` path silently cancels the stream and closes the results channel when its 16-slot buffer fills — the consumer cannot distinguish a backpressure disconnect from normal stream end. This is the most serious runtime finding.
- The Rust client validates only `protocol_status` on `invoke`; it never inspects `hresult` or the `MXSTATUS_PROXY` array, so a reply with an OK protocol envelope but failing per-item statuses reads as success — every other client raises. Conversely, .NET/Go/Java treat *any* nonzero HRESULT as failure (positive success codes like `S_FALSE` misclassified); only Python uses the correct `hresult < 0` COM semantics.
- No client exposes typed helpers for `WriteSecured`/`WriteSecured2` (single-item), `AuthenticateUser`, `ArchestrAUserToId`, `AdviseSupervisory`, `AddBufferedItem`, `SetBufferedUpdateInterval`, `Suspend`, or `Activate`, even though the wire contract supports all of them (`src/ZB.MOM.WW.MxGateway.Contracts/Protos/mxaccess_gateway.proto:150-160`). The secured-write parity path is reachable only via raw `Invoke`.
- Reconnect-with-replay is supported everywhere via `after_worker_sequence`, and four of five clients can re-attach a session wrapper to an existing session id; .NET cannot (its `MxGatewaySession` constructor is internal with no factory).
- Version constants drift in three clients: Go reports `0.1.0-dev`, Rust `version.rs` says `0.1.0-dev` while `Cargo.toml` says `0.1.2`, Python `version.py` says `0.1.0` while `pyproject.toml` says `0.1.2`. Only Java (0.2.0) is consistent.
- TLS posture is intentionally lenient but inconsistent across languages: .NET/Go/Java skip verification by default, Python does a blocking trust-on-first-use certificate pin, Rust is strict pin-only. The docs acknowledge this, but the Go CLI cannot even opt into strict validation.
- `docs/ClientPackaging.md` has drifted badly from the Python client's actual naming (package, module paths, CLI module) and the .NET solution filename.
## Cross-client parity matrix
| Capability | .NET | Go | Java | Python | Rust |
|---|---|---|---|---|---|
| Open/close session (typed + raw) | Yes | Yes | Yes | Yes | Yes |
| Re-attach wrapper to existing session id | **No** (internal ctor) | Yes (`NewSessionForID`) | Yes (`forSessionId`) | Yes (ctor) | Yes (`client.session()`) |
| Register / AddItem / AddItem2 / Advise / UnAdvise / RemoveItem | Yes | Yes | Yes | Yes | Yes |
| Unregister typed helper | **No** | Yes | Yes | Yes | **No** |
| Write / Write2 typed | Yes | Yes | Yes | Yes | Yes |
| WriteSecured / WriteSecured2 single-item typed | No | No | No | No | No |
| AuthenticateUser / ArchestrAUserToId typed | No | No | No | No | No |
| AdviseSupervisory / buffered / Suspend / Activate typed | No | No | No | No | No |
| Bulk add/advise/remove/unadvise/subscribe/unsubscribe | Yes | Yes | Yes | Yes | Yes |
| WriteBulk / Write2Bulk / WriteSecured(2)Bulk | Yes | Yes | Yes | Yes | Yes |
| ReadBulk | Yes | Yes | Yes | Yes | Yes |
| Sparse array write helper (`WriteArrayElements`) | Yes | Yes | Yes | Yes | Yes |
| Client-side 1000-item bulk cap | **No** | Yes | **No** | Yes | Yes |
| Event stream (idiomatic primitive) | `IAsyncEnumerable` | channel | iterator + observer | async iterator | `Stream` |
| Resume cursor (`after_worker_sequence`) | Yes | Yes | Yes | Yes | Yes |
| Automatic reconnect loop / `ReplayGap` handling | No | No | No | No | No |
| Alarm RPCs (Ack / StreamAlarms / QueryActiveAlarms) | Yes | Yes | Yes | Yes | Yes |
| Galaxy browse + lazy walker + WatchDeployEvents | Yes | Yes | Yes | Yes | Yes |
| Typed auth errors (Unauthenticated vs PermissionDenied) | Yes | **No** | Yes | Yes | Yes |
| MXAccess HRESULT/status-array validation | Yes (`!=0`) | Yes (`!=0`) | Yes (`!=0`) | Yes (`<0`, correct) | **No** |
| Automatic transient retry | Yes (Polly) | No | No | No | No |
| TLS default posture | skip-verify | skip-verify | skip-verify | TOFU pin | strict pin-only |
| Version constant matches package version | n/a (no version in csproj) | **No** (`0.1.0-dev`) | Yes (0.2.0) | **No** (0.1.0 vs 0.1.2) | **No** (`0.1.0-dev` vs 0.1.2) |
| Fixture-driven unit tests | Yes | Yes | Yes | Yes | Yes |
## Findings — .NET (`clients/dotnet`)
**D1 — Medium.** A session wrapper cannot be reconstructed from an existing session id, so reconnect-with-replay after client restart loses all typed helpers. `MxGatewaySession`'s constructor is `internal` (`clients/dotnet/ZB.MOM.WW.MxGateway.Client/MxGatewaySession.cs:19`) and no factory equivalent to Go's `NewSessionForID` (`clients/go/mxgateway/session.go:70`) or Java's `forSessionId` exists. Impact: the gateway's `DetachGraceSeconds`/replay features are usable only through the raw stub. Recommendation: add a public `MxGatewayClient.AttachSession(string sessionId)` factory.
**D2 — Medium.** `MxGatewaySession.DisposeAsync` calls `CloseAsync()` with no cancellation token or timeout (`clients/dotnet/ZB.MOM.WW.MxGateway.Client/MxGatewaySession.cs:881-885`); when the gateway is unreachable, `await using` disposal blocks for the full retry-pipeline budget and then throws from disposal, masking the original exception. Recommendation: swallow or time-bound close failures in the disposal path.
**D3 — Medium.** The retry budget self-defeats on timeouts: `ExecuteSafeUnaryAsync` caps the whole retry pipeline with `CancelAfter(Options.DefaultCallTimeout)` while each attempt also gets a `DefaultCallTimeout` deadline (`clients/dotnet/ZB.MOM.WW.MxGateway.Client/MxGatewayClient.cs:305-316` with `:269-303`), so a first attempt that ends in `DeadlineExceeded` exhausts the outer budget and the configured retries (`MxGatewayClientRetryPolicy.cs:62-67` lists `DeadlineExceeded` as retryable) never run. Recommendation: give the outer budget headroom (e.g., attempts × delay) or drop `DeadlineExceeded` from the retryable set.
**D4 — Medium.** `EnsureMxAccessSuccess` treats any nonzero HRESULT as failure (`clients/dotnet/ZB.MOM.WW.MxGateway.Client/MxCommandReplyExtensions.cs:32`), misclassifying positive COM success codes (e.g. `S_FALSE = 1`); Python's `hresult < 0` (`clients/python/src/zb_mom_ww_mxgateway/errors.py:133`) is the correct COM semantics. Same defect in Go (`clients/go/mxgateway/errors.go:121`) and Java (`.../client/MxGatewayErrors.java:50`). Impact: a parity-preserving gateway reply carrying a success HRESULT other than 0 throws in three clients and passes in one. Recommendation: align all clients on `hresult < 0`.
**D5 — Low.** No `<Version>` property in `clients/dotnet/ZB.MOM.WW.MxGateway.Client/ZB.MOM.WW.MxGateway.Client.csproj:14-28`; the published NuGet version (0.1.2) is supplied out-of-band at pack time, so the source tree does not record what ships. Recommendation: add `<Version>` to the csproj.
**D6 — Low.** Duplicate `InternalsVisibleTo` declared in both `Properties/AssemblyInfo.cs:3` and the csproj `AssemblyAttribute` block (`ZB.MOM.WW.MxGateway.Client.csproj:36-40`). Harmless but redundant; keep one.
**D7 — Low.** With `UseTls` and no CA file, the default (`RequireCertificateValidation = false`) installs an accept-all certificate callback (`clients/dotnet/ZB.MOM.WW.MxGateway.Client/MxGatewayClient.cs:361-364`). Documented and intentional for the internal-tool posture (`MxGatewayClientOptions.cs:31-36`), but it means `--tls` without a CA gives no server authentication. See cross-cutting C4.
Otherwise the .NET client is in good shape: retries are correctly restricted to idempotent commands (`MxGatewayClientRetryPolicy.cs:45-50`), the stream enumerators map `RpcException` per read and honor `EnumeratorCancellation` (`GrpcMxGatewayClientTransport.cs:78-109`), close is idempotent under a lock (`MxGatewaySession.cs:42-67`), style follows `CSharpStyleGuide.md` (sealed types, `Async` suffix, file-scoped namespaces), and the test project covers options, TLS handler, session, alarms, Galaxy, values, statuses, and CLI redaction.
## Findings — Go (`clients/go`)
**G1 — High.** `Session.Events()`/`EventsAfter()` silently terminate the stream on consumer backpressure: when the 16-slot results channel is full, `sendEventResult` cancels the stream and returns without queuing any terminal error, and the goroutine closes the channel (`clients/go/mxgateway/session.go:751-768`, spawn at `:700-742`). The consumer sees a closed channel — indistinguishable from graceful server end — and events are lost with no signal. The documented contract ("until … a terminal error is sent", `session.go:675-677`) does not mention this. Impact: silent data loss for any consumer that stalls for 16 events. Recommendation: enqueue a sentinel `EventResult{Err: ErrSlowConsumer}` before closing (guarantee one reserved slot), or drop the buffered-cancel variant in favor of the blocking `SubscribeEvents` path, which correctly rides gRPC flow control.
**G2 — Medium.** No typed auth error mapping: all RPC failures are wrapped in the generic `GatewayError` (`clients/go/mxgateway/errors.go:9-34`; e.g. `client.go:110-133`), so distinguishing `Unauthenticated` from `PermissionDenied` requires `status.Code(errors.Unwrap(err))`. `docs/ClientLibrariesDesign.md:153` requires the two be treated distinctly, and the other four clients expose typed auth errors. Recommendation: add `AuthenticationError`/`AuthorizationError` wrappers (or sentinel errors) in `errors.go`.
**G3 — Medium.** `Dial` uses deprecated `grpc.DialContext` with `grpc.WithBlock()` (`clients/go/mxgateway/client.go:60-68`); grpc-go ≥1.63 deprecates both in favor of `grpc.NewClient` with lazy connection. Impact: future grpc-go upgrades and `go vet`/staticcheck noise; blocking dial also hides per-RPC connection errors semantics that the rest of the ecosystem now expects. Recommendation: migrate to `grpc.NewClient` and surface readiness via a first `Ping`.
**G4 — Medium.** The CLI cannot opt into strict TLS validation: `dialForCommand` never sets `Options.RequireCertificateValidation` and no flag exists for it (`clients/go/cmd/mxgw-go/main.go:1151-1158`), so every non-CA-pinned TLS run of `mxgw-go` is skip-verify even though the library supports strictness (`clients/go/mxgateway/client.go:231-241`). Recommendation: add `-require-certificate-validation`.
**G5 — Low.** `ClientVersion = "0.1.0-dev"` (`clients/go/mxgateway/version.go:5-7`) is stale relative to the tagged module releases published via `scripts/tag-go-module.ps1`. Recommendation: bump on release as part of the tagging script.
**G6 — Low.** `newCorrelationID` swallows `crypto/rand` errors and returns an empty correlation id (`clients/go/mxgateway/session.go:786-792`); a fallback (timestamp counter) would preserve traceability.
**G7 — Low.** Nil-vs-empty asymmetry: `WriteBulk`/`ReadBulk` short-circuit an empty (non-nil) slice locally (`session.go:399-407`, `:524-532`) while `AddItemBulk`/`SubscribeBulk` send an empty command to the wire (`session.go:255-274`). Harmless but inconsistent within one file.
Otherwise the Go client is idiomatic: contexts propagate everywhere, `%w`-style wrapping via `Unwrap`, correct capped-timeout merging in `callContext` (`client.go:190-205`), CLI redacts the API key before JSON output (`cmd/mxgw-go/main.go:1162-1177`), and test coverage (session, TLS, galaxy, alarms, fixtures) is broad.
## Findings — Java (`clients/java`)
**J1 — Verified (no defect).** The JDK 17 retarget is complete and correct: `java.toolchain.languageVersion = 17` plus `options.release = 17` (`clients/java/build.gradle:20-31`) guarantees both language level and API surface are 17-bounded. A scan of all handwritten sources found no JDK 21+ APIs (no `SequencedCollection`/`getFirst`/`reversed()`, no virtual threads, no `ScopedValue`, no string templates, no `Math.clamp`); the language features present (records `MxStatuses.java:45`, pattern `instanceof`, `switch` expressions `MxGatewayErrors.java:17-25`) are all ≤17. Gradle/grpc/protobuf dependency versions (`build.gradle:5-11`) are 17-compatible.
**J2 — Medium.** Docs still claim Java 21 after the retarget: `clients/java/README.md:354` ("Java 21 Gradle toolchain"), `clients/java/JavaClientDesign.md:34-35`, and `docs/ClientPackaging.md:193`. CLAUDE.md's same-commit docs rule is violated on the retarget's own branch. Recommendation: sweep all three in this branch before merge.
**J3 — Medium.** The event-stream buffer is hardcoded to 16 with no configuration: `MxGatewayClient.streamEvents` constructs `new MxEventStream(16)` (`clients/java/zb-mom-ww-mxgateway-client/src/main/java/com/zb/mom/ww/mxgateway/client/MxGatewayClient.java:248`), and overflow cancels the RPC (`.../client/MxEventStream.java:124-132`). Unlike Go, the failure is at least surfaced as `MxGatewayException("…queue overflowed")`, but a consumer that stalls for 16 events is disconnected even though gRPC has native flow control that could simply be leaned on. Recommendation: make capacity an option and/or use `disableAutoRequestWithInitial`-style manual flow control instead of cancel-on-overflow.
**J4 — Low.** `MxEventStream` is a single-consumer iterator with unsynchronized `next` state (`MxEventStream.java:31`, `:65-92`); this constraint is not documented. Add a Javadoc note.
**J5 — Low.** `close()` initiates `shutdown()` without awaiting termination (`MxGatewayClient.java:346-351`); acceptable given `closeAndAwaitTermination()` exists (`:360-367`), but try-with-resources users can leak a channel briefly at JVM exit. Consider making `close()` await a short bound.
Otherwise the Java client is the most complete: blocking, future, and async stub variants with correct deadline layering (unary vs stream, `MxGatewayClient.java:418-430`), a full typed exception hierarchy with credential redaction (`MxGatewayErrors.java:13-29`, `MxGatewaySecrets.java`), the only in-process end-to-end CLI test harness (`zb-mom-ww-mxgateway-cli/src/test/java/.../InProcessGatewayHarness.java`), and consistent 0.2.0 versioning between Gradle (`build.gradle:16`) and `MxGatewayClientVersion.java:12`.
## Findings — Python (`clients/python`)
**P1 — Medium.** The default TLS path performs a blocking, unverified certificate fetch and pins it (trust-on-first-use) (`clients/python/src/zb_mom_ww_mxgateway/options.py:108-160`), and silently defaults the SNI override to `localhost` (`options.py:150-154`). This is documented and bounded (probe timeout, `asyncio.to_thread` in `client.py:61-63`), but it is the only client that opens a second out-of-band TCP+TLS connection per channel, and TOFU is vulnerable to first-contact interception. Recommendation: document the MITM window in the README threat model and prefer `ca_file` in examples.
**P2 — Low.** Version mismatch: `pyproject.toml` says `0.1.2` (`clients/python/pyproject.toml:9`) while `version.py` says `__version__ = "0.1.0"` (`clients/python/src/zb_mom_ww_mxgateway/version.py:3`), so `mxgw-py version` reports the wrong version. Recommendation: derive one from the other (e.g. `importlib.metadata.version`).
**P3 — Low.** `Session.close()` is not concurrency-safe (no lock around `_closed`, `session.py:38-55`) and repeated closes return a locally synthesized `CloseSessionReply` rather than the cached server reply — divergent from .NET/Go which cache the real reply. Minor; align with a cached-reply pattern.
**P4 — Low.** Circular-import workaround: `from .client import GatewayClient` at the bottom of `session.py:590` (`# noqa: E402`). Works, but a `TYPE_CHECKING` import plus a string annotation would remove the runtime cycle.
Otherwise the Python client is strong: correct `hresult < 0` semantics (`errors.py:133`), fully typed (`from __future__ import annotations`, precise unions), GIL-friendly pure-asyncio design with stream iterators that cancel the call on generator exit (`client.py:230-263`), packaging correctly capped at `setuptools <77` for the ≤2.3 metadata constraint (`pyproject.toml:2-5`), and the largest per-client test suite (13 test modules including regression files for prior review findings).
## Findings — Rust (`clients/rust`)
**R1 — High.** The published crate cannot build outside this repository. `build.rs` resolves the protos two directories above the crate (`clients/rust/build.rs:8-16`: "clients/rust must live two levels below the repository root") and `src/generated.rs` is only `tonic::include_proto!` of build output (`clients/rust/src/generated.rs:16-40`); `Cargo.toml` does not vendor the `.proto` files into the package. The packaging script masks this by using `cargo package --no-verify` and `cargo publish --no-verify` (`scripts/pack-clients.ps1:190-211`). Any consumer of the Gitea-published `zb-mom-ww-mxgateway-client 0.1.2` fails in `build.rs` at first `cargo build`. Recommendation: copy the three protos into the crate (e.g. `clients/rust/protos/`, listed in `include`), fall back to them when the repo path is absent, and remove `--no-verify` so `cargo package` verifies buildability.
**R2 — High.** `invoke` validates only `protocol_status` and never inspects `hresult` or the `MXSTATUS_PROXY` array: `ensure_command_success` checks `code == Ok` only (`clients/rust/src/error.rs:214-226`, used by `client.rs:177-179`). Every other client performs a second MXAccess-level check (e.g. `MxCommandReplyExtensions.cs:27-41`, `errors.go:117-130`, `MxGatewayErrors.java:46-58`, `errors.py:122-148`), because a reply can carry an OK protocol envelope with failing per-item statuses. Impact: a Rust `session.write(...)` can report success while MXAccess rejected the write; there is also no distinct MxAccess error variant to catch. Recommendation: add an `ensure_mxaccess_success` pass (hresult `< 0` + statuses) and an `Error::MxAccess` variant.
**R3 — Low.** `CLIENT_VERSION = "0.1.0-dev"` with a doc comment claiming it "Mirrors Cargo.toml" while `Cargo.toml` says `0.1.2` (`clients/rust/src/version.rs:6-7` vs `clients/rust/Cargo.toml:3`). Recommendation: `pub const CLIENT_VERSION: &str = env!("CARGO_PKG_VERSION");`.
**R4 — Low.** No `unregister` typed helper (session surface at `clients/rust/src/session.rs:119-231` covers register/add/advise/remove but not Unregister); Go/Java/Python have it. Add for symmetry.
**R5 — Low.** The CLI is a single 2,699-line `main.rs` (`clients/rust/crates/mxgw-cli/src/main.rs`) — the largest single file in the client tree; the Windows stack-size workaround it forced (`clients/rust/.cargo/config.toml:1-19`) is itself evidence the command enum has outgrown one module. Split subcommands into modules.
Otherwise the Rust client is clean: clippy-conscious generated-module allowances (`generated.rs:18`, `#![warn(missing_docs)]` in `lib.rs:12`), a well-structured `thiserror` enum with boxed `tonic::Status`, credential scrubbing in error messages with a unit test (`error.rs:256-289`), cheap `Clone` client over a shared channel, correct unary-vs-stream timeout split (`client.rs:280-293`), and bulk caps enforced client-side (`session.rs:29`).
## Findings — cross-cutting
**X1 — High.** The MXAccess command parity gap is uniform: no client exposes typed helpers for single-item `WriteSecured`/`WriteSecured2`, `AuthenticateUser`, `ArchestrAUserToId`, `AdviseSupervisory`, `AddBufferedItem`, `SetBufferedUpdateInterval`, `Suspend`, or `Activate`, although the contract defines all of them (`src/ZB.MOM.WW.MxGateway.Contracts/Protos/mxaccess_gateway.proto:150-160`) and `gateway.md` documents `AdviseSupervisory` as a precondition for user-attributed plain writes. Only the .NET, Go, and Python CLIs offer `advise-supervisory` via hand-built raw commands (`clients/go/cmd/mxgw-go/main.go:364-391`, `clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli/MxGatewayClientCli.cs:456-470`, `clients/python/src/zb_mom_ww_mxgateway_cli/commands.py:287-291`); Java and Rust CLIs lack even that. Impact: the secured-write path (AuthenticateUser → WriteSecured) and the buffered-event family (`OnBufferedDataChange`) cannot be exercised through any typed client API. Recommendation: add the missing typed session helpers to all five clients, starting with `adviseSupervisory`, `writeSecured`, and `authenticateUser`.
**X2 — Medium.** No client handles the `ReplayGap` sentinel or offers a reconnect loop. All five expose the `after_worker_sequence` resume cursor (e.g. `MxGatewaySession.cs:865-876`, `session.go:682-688`, `MxGatewaySession.java:718-722`, `session.py:571-582`, `session.rs:648`), which matches the "no client-side reconnect" v1 non-goal in `docs/ClientLibrariesDesign.md:64-70`, but since the gateway shipped `DetachGraceSeconds` + replay, none of the clients document or type the `ReplayGap` event a resuming consumer must expect (`grep ReplayGap clients/` → no handwritten hits). Recommendation: at minimum document gap detection per client README; longer term add a resume helper.
**X3 — Medium.** `docs/ClientPackaging.md` has drifted from reality: Python package named `mxaccess-gateway-client` and generated dir `src/mxgateway/generated` (`docs/ClientPackaging.md:159-160`) vs actual `zb-mom-ww-mxaccess-gateway-client` (`clients/python/pyproject.toml:8`) and `src/zb_mom_ww_mxgateway/generated`; CLI module `python -m mxgateway_cli` (`ClientPackaging.md:187`) vs actual `zb_mom_ww_mxgateway_cli`; .NET solution `ZB.MOM.WW.MxGateway.Client.sln` (`ClientPackaging.md:51-52`) vs actual `.slnx`; Java 21 (`:193`, see J2). `docs/ClientLibrariesDesign.md:410` repeats the stale Python generated path. Recommendation: one doc sweep commit.
**X4 — Medium.** TLS default posture is inconsistent across the five clients: accept-any-cert in .NET (`MxGatewayClient.cs:361-364`), Go (`client.go:236-240`, `InsecureSkipVerify`), and Java (`MxGatewayClient.java:387-395`, `InsecureTrustManagerFactory`); TOFU pinning in Python (`options.py:133-154`); strict pin-only in Rust. `docs/CrossLanguageSmokeMatrix.md:58-66` documents the divergence, but the practical result is that the same `--tls`-without-CA invocation authenticates the server in one language, half-authenticates in another, and not at all in three. Recommendation: converge on the Python TOFU model or at least emit a one-line warning when verification is disabled.
**X5 — Low.** Client-side bulk caps differ: Go/Python/Rust enforce 1,000 items locally (`session.go:19`, `session.py:11`, `session.rs:29`) while .NET and Java send unbounded lists and rely on the gateway. Harmless but produces different error types for the same oversized call. Align (either all enforce or none).
**X6 — Low.** Event-stream backpressure semantics differ by language: Go buffered-16/silent-cancel (G1), Java buffered-16/error-cancel (J3), .NET/Python/Rust unbuffered (native gRPC flow control, pushing backpressure to the gateway where the documented fail-fast policy applies). The per-language behavior under a slow consumer is a parity-relevant observable and belongs in `docs/ClientBehaviorFixtures.md`.
**X7 — Low.** Generated-code hygiene is good everywhere: `clients/go/internal/generated`, `clients/java/src/main/generated` (6 tracked files), `clients/python/src/zb_mom_ww_mxgateway/generated` exist and match the manifest; Rust generates into `OUT_DIR` with a `.gitkeep` placeholder; `clients/dotnet/generated` is intentionally absent because the .NET client references the Contracts project directly (`ZB.MOM.WW.MxGateway.Client.csproj:4`), which `docs/ClientPackaging.md:38-40` correctly describes. Python `build/` and `.pytest_cache/` exist on disk but are not git-tracked.
## Top 5 recommendations
1. **Fix Rust crate publishability (R1):** vendor the three `.proto` files into the crate, add a repo-path fallback in `build.rs`, and drop `--no-verify` from `scripts/pack-clients.ps1` so `cargo package` proves the crate builds standalone.
2. **Fix Go silent stream termination (G1):** reserve a slot for a terminal `EventResult{Err: ...}` before closing the `Events()` channel so slow-consumer disconnects are observable, or deprecate the buffered-cancel path in favor of `SubscribeEvents`.
3. **Add MXAccess-level reply validation to Rust and align HRESULT semantics everywhere (R2, D4):** Rust must check `hresult` and the status array; .NET/Go/Java should switch from `hresult != 0` to `hresult < 0` to match COM semantics and Python.
4. **Close the typed-command parity gap (X1):** add `adviseSupervisory`, single-item `writeSecured`/`writeSecured2`, and `authenticateUser` helpers to all five session APIs (the wire already supports them), then the buffered-item family.
5. **One doc-and-version sweep on the retarget branch (J2, X3, G5, P2, R3):** update the three "Java 21" references, the Python naming in `ClientPackaging.md`/`ClientLibrariesDesign.md`, the `.sln``.slnx` reference, and reconcile the Go/Rust/Python version constants with their published package versions.