# Code Review — Driver.MTConnect | Field | Value | |---|---| | Module | `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect` | | Reviewer | Claude (per-module review) | | Review date | 2026-08-10 | | Commit reviewed | `b990adae` | | Status | Complete | | Open findings | 0 | ## Checklist coverage A comprehensive review completes every category, recording "No issues found" where a category produced nothing rather than leaving it blank. | # | Category | Result | |---|---|---| | 1 | Correctness & logic bugs | 3 findings — `Driver.MTConnect-002`, `Driver.MTConnect-003`, `Driver.MTConnect-006` | | 2 | OtOpcUa conventions | No issues found. `DriverType` and the factory's `DriverTypeName` are both aliased to `DriverTypeNames.MTConnect` (the `TwinCat`/`Focas` drift fix); every status code is a NAMED `const` matching an `Opc.Ua.StatusCodes` member, which the `StatusCodeParityTests` reflection guard requires; there is deliberately no `JsonStringEnumConverter` on the config reader, so enum-carrying DTO fields stay `string?` and go through `ParseEnum`. Authorization is never decided here — the driver reports `SecurityClassification` as browse metadata only, and says so explicitly. .NET 10 / AnyCPU, Serilog via `ILogger` with a `NullLogger` default, no backend NuGet. | | 3 | Concurrency & thread safety | 1 finding — `Driver.MTConnect-005`. The lock discipline is otherwise sound and documented per field: `_lifecycle` (non-reentrant, never entered from the pump or the probe loop), `_subscriptionLock` (never held across an await or a callback; lock order `_lifecycle` → it; released before `StopSampleStreamAsync`), `_probeLock` (field read/write only). `_session` / `_binding` / `_index` are immutable snapshots published by `Volatile.Write` or compare-and-swap, so a lock-free reader cannot pair one initialize's client with another's model. Subscriber callbacks are raised outside every lock with the invocation list walked by hand, so one throwing consumer cannot starve the others. | | 4 | Error handling & resilience | 1 finding — `Driver.MTConnect-001`. Otherwise strong: failure is never dressed as success (a `/probe` that succeeds while the priming `/current` fails is Faulted, not "healthy with no data"); a quiet end of the `/sample` enumeration is reported rather than read as idle; an unreadable config document faults only a driver that had no working session; `Faulted` is reserved for a failed initialize because `DriverHealthReport` maps it to a `/readyz` 503; `/current` and `/sample` own separate degrade flags so neither papers over the other's failure. | | 5 | Security | 1 finding — `Driver.MTConnect-004` (a capability gate nobody can turn on, not a weakness). No credential is logged: `MTConnectDriverProbe` builds every message from a scheme/host/port/path redaction that excludes `Uri.UserInfo`, and its last-resort catch forwards only the exception TYPE. The Agent's refusal text reaches a log line, never `DriverHealth.LastError`. Write-back is off at two independent gates and CONDITION items are excluded. `XDocument.Parse`/`Load` run under the BCL default `DtdProcessing.Prohibit`, so the XML paths are not XXE-exposed, and the multipart reader caps its buffer at 32 MB against a hostile Agent. Every caller-supplied URL part is percent-encoded at the seam. | | 6 | Performance & resource management | 1 finding — `Driver.MTConnect-007`. Otherwise careful: one `/current` per read batch whatever the batch size; one shared `/sample` long poll behind every subscription handle; `FrozenDictionary` for the RawPath↔dataItemId tables; a separator-insensitive comparer that avoids allocating on the discovery hot path; a multipart reader that never buffers more than the part in flight. Each `CancellationTokenSource` is disposed only once its loop is provably gone (an abandoned loop still holds the token), and the two `HttpClient`s exist precisely so the unary deadline stays bounded while the long poll is not. | | 7 | Design-document adherence | 1 finding — `Driver.MTConnect-008` (stale claims, corrected). The implementation otherwise matches `docs/drivers/MTConnect.md` and `docs/plans/2026-08-05-mtconnect-writeback-design.md`: `IWritable` is observation injection through `AllowPut`, doubly gated and CONDITION-excluded; `RediscoverPolicy.Once` plus an `instanceId` watch; the dormant discovered-node injection path is correctly not relied on. | | 8 | Code organization & conventions | No issues found. Capability seams are implemented on the concrete type and the factory deliberately returns it un-narrowed, which is why `IWritable` was dispatched with no factory edit. The Options pattern is honoured, the config document has exactly one parser (`ParseOptions`, which the factory delegates to rather than mirroring), and the transport sits behind `IMTConnectAgentClient` so the whole driver is exercised with no socket. | | 9 | Testing coverage | No issues found. 538 tests across 14 classes cover the lifecycle, read, write, subscribe, discovery, probe, both parsers, the observation index and the multipart framing; each way the stream can be knocked off sequence (gap, `OUT_OF_RANGE`, restart, drop, not-supported) has a test, several with an explicit falsification control. Nothing waits on wall-clock time — the pump exposes its task as the teardown barrier and the probe cadence is an injected scheduler. The two gaps this review found are `Driver.MTConnect-001` / `-002`; both now have tests, each verified red first. | | 10 | Documentation & comments | 1 finding — `Driver.MTConnect-008`. The XML documentation is unusually thorough and states the reasoning (why a lock is not reentrant, why an exception beats an empty result, why a fallback exists) rather than restating the code. Both stale items found were premises another module had since invalidated, not drifted descriptions of this code. | ## Findings ### Driver.MTConnect-001 | Field | Value | |---|---| | Severity | Medium | | Category | Error handling & resilience | | Location | `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriver.cs:1932` | | Status | Resolved | **Description:** The `/sample` pump's `StreamVerdict.Reopen` arm reset the reconnect ladder unconditionally (`attempt = 0; _reconnectAttempts = 0;`) and reopened the stream with no backoff. That is right for the two conditions the verdict was written for — a sequence gap and an Agent `OUT_OF_RANGE` rejection — because the `/current` re-baseline genuinely resolves both, so neither recurs immediately. But the `catch (InvalidDataException)` in `ConsumeStreamAsync` that routes `OUT_OF_RANGE` to a re-baseline also catches two conditions a `/current` cannot resolve: a chunk `MTConnectStreamsParser` cannot parse (thrown from inside the `await foreach`) and a part larger than the multipart reader's 32 MB cap. Against an Agent producing either, the pump ran `/sample` → parse failure → `/current` (succeeds) → `Reopen` with the ladder reset → `/sample` again, **with no delay at any point** — an unbounded hot loop issuing two HTTP requests per iteration as fast as the Agent can answer, and one Warning per iteration. That is exactly the failure mode every other arm of this pump is written to prevent (the `_streamUnsupported` latch, `BackoffGrowthFloorMs`, the non-positive-cap fallback). **Recommendation:** Use the discriminator the transient arm already uses — did *this* connection deliver a chunk? A re-baseline behind a stream that delivered stays an immediate reopen on a clean ladder; one on a connection that delivered nothing advances `BackoffFor` like any other failure. **Resolution:** Fixed `6c63bf8f`, 2026-08-10 — the `Reopen` arm now sets `attempt = outcome.DeliveredAny ? 0 : attempt + 1`, so a re-baseline that resolved nothing backs off while a buffer roll behind a working stream still reopens instantly. Pinned by `A_rebaseline_that_delivers_nothing_still_advances_the_reconnect_backoff` (red before the change: `ReconnectAttempts` stayed 0 across six stream generations) plus the falsification control `A_rebaseline_after_a_delivered_chunk_still_reopens_immediately`, which fails any "back off on every re-baseline" over-correction. ### Driver.MTConnect-002 | Field | Value | |---|---| | Severity | Medium | | Category | Correctness & logic bugs | | Location | `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriverProbe.cs:192` | | Status | Resolved | **Description:** `MTConnectDriverProbe` (the AdminUI Test Connect probe) parsed only `agentUri` from the driver config and requested `{AgentUri}/probe`, while `MTConnectAgentClient` narrows every real request to `{AgentUri}/{DeviceName}/probe`. A driver scoped to a device therefore had its reachability tested against a **different URL from the one it will use**. The consequence is the failure-that-looks-like-success shape: a mistyped, since-renamed, or not-yet-deployed `deviceName` shows GREEN on Test Connect and then Faults the driver at `InitializeAsync` — a cppagent answers an unknown device path with an `MTConnectError` document under HTTP 200, which the parser surfaces as `InvalidDataException`. The type's own "reachable ≠ deployable" remark did not cover this: the examples it names (`requestTimeoutMs`, a malformed tag entry) do not change the endpoint, and which endpoint is reached is the one thing a reachability probe exists to establish. **Recommendation:** Read `deviceName` into the probe's minimal DTO and pass it into the `MTConnectDriverOptions` handed to `MTConnectAgentClient`, so the probe issues the driver's own request path. Keep the deliberate independence from `ParseOptions` — the distinction is "which keys compose the URL", not "which keys are cheap to read". **Resolution:** Fixed `6c63bf8f`, 2026-08-10 — the probe DTO gained `DeviceName` and forwards it, and the type remarks now state the URL-composing rule. Pinned by `Configured_deviceName_scopes_the_probe_request_the_way_the_driver_does` (red before the change: requested `/probe`, expected `/VMC-1/probe`) and the control `An_absent_deviceName_leaves_the_probe_agent_wide`. ### Driver.MTConnect-003 | Field | Value | |---|---| | Severity | High | | Category | Correctness & logic bugs | | Location | `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/RawBrowseCommitMapper.cs:256` (cause) · `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriver.cs:3286` (`BuildBinding` precedence) | | Status | Resolved | **Description:** **Every browse-committed MTConnect tag is coerced to `String` at runtime while its OPC UA node is typed from the probe inference**, so a browsed `SAMPLE` publishes text into a `Double` node. `RawBrowseCommitMapper.MapLeaf` does two things with one browsed leaf: - it sets the raw tag row's OPC UA `DataType` from the leaf's inferred `DriverDataType` (`MapDataType`: `Float64` → `Double`, `Int64` → `Int64`); and - it calls `BuildTagConfig`, whose MTConnect branch is `new MTConnectTagConfigModel { FullName = address }.ToJson()`. `MTConnectTagConfigModel.DataType` defaults to `DriverDataType.String` and `ToJson()` **unconditionally** writes `dataType`, so the committed blob is `{"fullName":"dev1_pos","dataType":"String"}`. That is a non-blank authored type, so `MTConnectTagDefinitionFactory.TryFromTagConfig` returns a definition, `BuildBinding` installs it as "the deploy-time authority", and step 3 of the documented coercion precedence — the Agent's own `/probe` declaration via `MTConnectDataTypeInference` — is **never reached for a browse-committed tag**. The driver's own remark that this step "is what makes a browse-committed tag work" rested on a premise (`RawBrowseCommitMapper` writes only the address) that was true when written and is false at HEAD. This is the same defect class as Gitea #536, which is exactly why the AbCip, TwinCAT and FOCAS branches of `BuildTagConfig` forward the browsed native type via `NativeType(addressFields, …)`. MTConnect was given the typed-model branch without that forwarding. Severity is High rather than Medium because the address picker is the primary authoring path for this driver, the mistyping is silent (no warning at authoring and none at the deploy gate — the blob is perfectly valid), and the resulting node advertises a numeric type it will never carry. **Recommendation:** Cross-module — do not fix in this driver. In `RawBrowseCommitMapper`, forward the browsed leaf's `DriverDataType` into the MTConnect branch as the other three typed-model branches already do. `MapLeaf` already holds `driverDataType`; either thread it into `BuildTagConfig` alongside `addressFields`, or supply it through `addressFields` under the `dataType` key `NativeType` already reads. Add the MTConnect case to whatever test pins the #536 behaviour for AbCip/TwinCAT/FOCAS, and assert the **pair** — that the tag row's OPC UA type and the blob's coercion type agree — rather than either alone: asserting only the blob would pass against a mapper that had stopped setting the row's type. **Resolution:** Fixed `64be41d8`, 2026-08-10 (Gitea **#560**) — cross-module, in `ZB.MOM.WW.OtOpcUa.AdminUI`: `RawBrowseCommitMapper.MapLeaf` now threads the browsed leaf's `driverDataType` into `BuildTagConfig`, whose MTConnect branch uses it as the `NativeType` fallback, so the committed blob's coercion type and the raw tag row's OPC UA type both come from the one probe inference — the same forwarding the AbCip / TwinCAT / FOCAS branches already did for #536. A leaf inferred (or reported) as `String` still commits `"dataType":"String"`, and an unusable name still falls back to it through the shared numeric-string / unparseable guards. Pinned by `RawBrowseCommitMapperTests` asserting the PAIR plus that `String` control; the stale premises this defect left inside these two modules are corrected under `Driver.MTConnect-008` and `Driver.MTConnect.Contracts-001`. ### Driver.MTConnect-004 | Field | Value | |---|---| | Severity | Medium | | Category | Design-document adherence | | Location | `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Forms/MTConnectDriverForm.razor:1` · `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts/MTConnectDriverOptions.cs:146` | | Status | Resolved | **Description:** `IWritable` shipped for this driver on 2026-08-05 (`b29623cf`) behind `Write.Enabled`, and `docs/drivers/MTConnect.md` §"Two independent gates" tells the operator to set `"Write": { "Enabled": true }` in the driver config. **There is no way to author that through the product.** `MTConnectDriverForm` is the only editor `DriverFormMaps` maps for this driver type, it exposes no write section, and its header comment still asserts "MTConnect is read-only — there are no write knobs to author". `DriverConfigModal` renders the typed form only (no raw-JSON escape hatch), and the AdminUI exposes no driver-instance API — its only `Map*` endpoints are the deploy and script-analysis ones. So the documented enablement path exists only as a direct edit of `DriverInstance.DriverConfig` in the config DB. Mitigating, and worth knowing before fixing: `MTConnectDriverForm.BuildConfigJson` preserves unknown **top-level** keys, so a `write` block introduced by any other means is *not* destroyed by a later edit through the form. The capability is unreachable rather than fragile. **Recommendation:** Cross-module — add a "Write-back" section to `MTConnectDriverForm` with an `Enabled` switch and form text stating that it is observation injection, that the Agent's own `AllowPut` is the other gate, and that it does not by itself make any node writable over OPC UA (`Tag.AccessLevel` still composes the `CurrentWrite` bit). Add `Write` to the form's mirrored `MTConnectConfigDto` so the value round-trips, and delete the now-false "MTConnect is read-only" comment. Until then the driver-side gate is correct and fails closed, which is why this is Medium rather than High. **Resolution (2026-08-10):** Fixed `298ad04f`. `MTConnectDriverForm` gained a **Write-back** section — a switch plus form text stating that a write is observation injection, that the Agent's own `AllowPut` is the independent second gate, and that arming it does not by itself make any node writable over OPC UA (`Tag.AccessLevel` still composes `CurrentWrite`; CONDITION items never are). `Write` was added to the form's mirrored `MTConnectConfigDto` and to `MTConnectFormModel`, the false "MTConnect is read-only" header comment is gone, and `docs/drivers/MTConnect.md` §"Two independent gates" now names the section instead of implying a config-DB edit. ⚠️ **The block is written in BOTH states, unlike the timing knobs.** Those are omitted when unusable so `ParseOptions` substitutes the driver's own default; an omitted `write` block would instead leave a previously-authored `"enabled": true` standing — the switch would appear to turn writing off while the deployed driver went on accepting writes. Pinned by `Turning_write_back_off_clears_a_previously_authored_true`, the falsification control for that decision. The finding's own mitigation (unknown top-level keys survive an edit) is likewise not sufficient on its own: `FromJson` had to LEARN the block, or the modal would show the switch off over a config that has writing on — `Write_back_round_trips_and_a_hand_authored_block_loads_into_the_form`. This project has no bUnit, so the seam is unit-tested from both ends rather than through the page: `MTConnectDriverFormModelTests` asserts the exact bytes `BuildConfigJson` emits, and `MTConnectFactoryTests.The_document_the_AdminUI_form_emits_arms_write_back` parses that same document through `MTConnectDriver.ParseOptions` and asserts `MTConnectDriverOptions.Write.Enabled` is true (then false for the switched-off variant). The literal is duplicated because a driver test project cannot reference the AdminUI; `The_write_enabled_document_is_the_literal_the_driver_test_parses` pins the two copies together so a rename fails on the authoring side, naming the literal to update. **Residual: live page verification is still a rig step** — the razor markup itself (the switch's binding and `EmitAsync` wiring) is not covered by any test here. ### Driver.MTConnect-005 | Field | Value | |---|---| | Severity | Low | | Category | Concurrency & thread safety | | Location | `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriver.cs:2655` · `:689` · `:797` · `:1425` | | Status | Resolved | **Description:** `_options` is the one piece of shared driver state published by a **plain** field write (`_options = options;` at the `StartCoreAsync` commit point) and read lock-free from other threads — `ReadAsync`, `WriteAsync`, `DiscoverAsync`, `HostName`, `DegradeAfterReadFailure`. Every other value published at that same commit point (`_session`, `_binding`, `_index`, `_announcedInstanceId`) uses `Volatile.Write` and is read with `Volatile.Read`, and the field remarks explain at length why that matters. The practical exposure is small — a reference-typed field cannot tear, and the `Volatile.Write` of `_binding` two statements later acts as a release fence for it — but a reader that reads `_options` *before* it reads `_binding` (which both `ReadAsync` and `WriteAsync` do) has no acquire ordering between them, so it can in principle judge a batch against a stale options document paired with the new tag table. The worst observable outcome is one batch judged against the previous `Write.Enabled` or `RequestTimeoutMs`. **Recommendation:** Publish and read `_options` with `Volatile.Write` / `Volatile.Read` like its three siblings, or fold it into the `AgentSession` / `TagBinding` snapshot so the "read once, as one consistent unit" rule this file already argues for covers it too. Record-only for now: the change touches ~10 read sites and the exposure does not justify unreviewed churn. **Resolution (2026-08-10):** Fixed `2d7dffcc`. Both writes are `Volatile.Write` (the constructor's as well as the commit point's) and all thirteen reads are `Volatile.Read`; the field gained the remark its three siblings already carried. Two sites took a local snapshot rather than repeating the read — the `_streamUnsupported` refusal, which named the Agent twice, and `GetOrFetchProbeModelAsync`. **A source scan is the pin, deliberately.** The defect is a missing fence, and no test can force the interleaving that exposes it on x86/arm64 in bounded time; what actually regresses is a NEW read site written without the fence. `MTConnectOptionsPublicationTests` therefore fails on any unguarded touch of `_options` in `MTConnectDriver.cs` — the same shape as `LibplctagGatewayConsumptionGuardTests`, and red before the change with all 15 sites listed by line. It carries a floor count so the scan cannot go blind if the field is renamed. ### Driver.MTConnect-006 | Field | Value | |---|---| | Severity | Low | | Category | Correctness & logic bugs | | Location | `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriver.cs:914` | | Status | Resolved | **Description:** `TryFormatValue` refuses `null` and `Array`, then renders everything else with `Convert.ToString(value, InvariantCulture)`. Its own remark gives the right reason for the array guard — `Convert.ToString` falls back to `object.ToString()` for a non-`IConvertible` value, so an array would be injected into the Agent as the literal text `System.Double[]`, which the Agent stores happily and which stays invisible until someone reads it back. That reasoning is not specific to arrays: any other non-`IConvertible` value reaching this method (an `Opc.Ua.ExtensionObject`, an `XmlElement`, a `List` from a non-SDK caller) is injected as its type name under a Good status. Not reachable today by the production path — the SDK coerces a written value against the node's `DataType` before `DriverInstanceActor.HandleWriteAsync` builds the `WriteRequest`, and every MTConnect tag is a scalar Boolean/numeric/String — hence Low. **Recommendation:** Refuse on the property the remark actually relies on rather than on one type that happens to have it: `if (value is null or Array or not IConvertible) return false;`. `string`, `bool`, every numeric and `DateTime` are `IConvertible`, so the accepted set is unchanged. **Resolution (2026-08-10):** Fixed `2d7dffcc` exactly as recommended (`Array` kept in the pattern as documentation of the shape this was first noticed on, though it is subsumed). Red first: `A_non_convertible_composite_value_is_refused_as_a_type_mismatch` wrote a `List` and got `Good` back, having injected the list's own type name into the Agent as the observation value. The falsification control is the load-bearing half — `An_IConvertible_scalar_still_writes` plus `A_DateTime_still_writes_as_its_invariant_string` fail any over-correction expressed as "is this a primitive I recognise", which would start refusing real writes. ⚠️ **Worth knowing: the fix stranded the vehicle of an existing test.** `Cancellation_mid_batch_leaves_the_slots_already_written_with_their_real_outcomes` injected its cancellation from a value's own `ToString`, and such a value is not `IConvertible` — so it was refused before it was ever rendered and the cancellation never happened. The injection point moved to the Agent call (`CannedAgentClient.AfterWrite`), which is the seam that test is asserting around anyway; its determinism property is unchanged. ### Driver.MTConnect-007 | Field | Value | |---|---| | Severity | Low | | Category | Performance & resource management | | Location | `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectAgentClient.cs:85` | | Status | Resolved | **Description:** `MTConnectAgentClient.Dispose()` is a plain check-then-set on a non-volatile `_disposed` field, so two concurrent disposals can both pass the guard. The second then calls `_disposeCts.Cancel()` on an already-disposed `CancellationTokenSource`, which throws `ObjectDisposedException`. The class documents that disposal is safe against an in-flight *enumeration* but claims nothing about concurrent disposal, and in practice the driver disposes only under `_lifecycle` and wraps the call in `SafeDispose`, so the throw is caught and traced — hence Low rather than Medium. **Recommendation:** Guard with `Interlocked.Exchange(ref _disposed, 1) != 0` over an `int` field, or state on `Dispose` that it is not thread-safe and is called only under the driver's lifecycle lock. **Resolution (2026-08-10):** Fixed `2d7dffcc` — `_disposed` is an `int` claimed with `Interlocked.Exchange`, the four request seams read it with `Volatile.Read`, and `Dispose` now states that it is idempotent under a race. Pinned by `Concurrent_disposals_are_idempotent`. **Its budget is part of the fix.** The window is a handful of instructions and the loser only throws if it reaches `Cancel()` after the winner has finished disposing two `HttpClient`s, so a small race proves nothing: four racers over 200 rounds passed against the DEFECTIVE code. Eight racers on a barrier over 3000 rounds failed at round 690, which is the red this resolution rests on. Once the claim is atomic no schedule can fail it, so the test cannot flake in either direction — but do not shrink the budget. ### Driver.MTConnect-008 | Field | Value | |---|---| | Severity | Low | | Category | Documentation & comments | | Location | `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriver.cs:3210` · `docs/drivers/MTConnect.md:169` · `MTConnectDriver.cs:1227`/`:1245`/`:1255` | | Status | Resolved | **Description:** Three stale or self-contradicting pieces of documentation, each of which would lead a reader to a wrong conclusion: 1. `BuildBinding`'s remarks state that the probe-inference step of the coercion precedence "is what makes a browse-committed tag work: `RawBrowseCommitMapper` writes only the address into the blob". That premise is false at HEAD and the step is in fact dead for browse-committed tags — see `Driver.MTConnect-003`. 2. `docs/drivers/MTConnect.md` §"FullName == DataItem `id`" says `address` "is the field name `RawBrowseCommitMapper` writes" for MTConnect. It writes `fullName`; `address` is the legacy spelling that must stay readable. 3. `DiscoverAsync` reads `var options = _options;` once, with an explicit comment that a concurrent re-initialize must not scope half the walk to one device and half to another — then logs `_options.AgentUri` (the field) at all three of its log sites. Harmless in effect, but it contradicts the rule stated two lines above it, which is how such a rule gets broken later. **Recommendation:** Correct (1) and (2) to describe HEAD and cross-reference the live defect rather than deleting the rule, which is still correct for hand-authored, CSV and API blobs; make (3) use the snapshot it deliberately took. **Resolution:** Fixed `6c63bf8f`, 2026-08-10 — `BuildBinding` and `docs/drivers/MTConnect.md` now carry an explicit warning naming `Driver.MTConnect-003` and the mapper branch responsible, and the three `DiscoverAsync` log sites use the once-read `options` snapshot.