# Changelog All notable changes to the `mxaccess` workspace are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the workspace as a whole follows [SemVer](https://semver.org/) but the 0.0.x line is pre-release / API-unstable. ## [Unreleased] — V1 — 2026-05-07 V1 is the first publishable cut. Closes M0 → M6 from `design/60-roadmap.md`. The workspace stays at `version = "0.0.0"` indefinitely (F48 — internal usage only, no crates.io publish; consumers depend via path or git). ### Added - **`mxaccess-codec`** — pure protocol codec covering `MxReferenceHandle`, `NmxTransferEnvelope`, `NmxItemControlMessage`, `NmxWriteMessage` (scalar + array, normal + timestamped), `NmxSecuredWrite2Message`, `NmxSubscriptionMessage` (single + multi-record DataUpdate per F44), `NmxReferenceRegistrationMessage`, `NmxMetadataQueryMessage`, `NmxOperationStatusMessage`, `ObservedWriteBodyTemplate`, ASB Variant + `AsbStatus` + `RuntimeValue`, `MxStatus`, `MxValueKind`, `MxDataType`, `MxValue`. Counting-allocator bench harness in `benches/alloc_count.rs` (F38) reports 1–4 allocs per write across the proven matrix, well under the R12 < 5/write target. - **`mxaccess-rpc`** — DCE/RPC PDU codec, NTLMv2 client + server-direction packet-integrity verify (F2 with `subtle::ConstantTimeEq`), TCP transport, OBJREF parser + Win32 `CoMarshalInterface` emitter (F6), `IObjectExporter::ResolveOxid` + `ResolveOxid2` (F10), `IRemUnknown::RemQueryInterface` + `RemAddRef`/`RemRelease` (F11). - **`mxaccess-callback`** — RPC server hosting `INmxSvcCallback` + `IRemUnknown` for inbound `DataReceived` / `StatusReceived` frames. `dcom_sink` (F55 Path A, gated by `windows-com`) hosts the callback as a DCOM-managed object so `RegisterEngine2` accepts it on AVEVA installs that do SCM-side OXID resolution against RPCSS; the hand-rolled `CallbackExporter` is retained for unit tests. - **`mxaccess-nmx`** — `INmxService2` client (`RegisterEngine2`, `TransferData`, `AddSubscriberEngine`, `SetHeartbeatSendInterval`, etc.) plus auto-resolving `NmxClient::create` factory (F12, gated by `windows-com`). - **`mxaccess-galaxy`** — `tiberius`-backed `Resolver` + `UserResolver` (F14, gated by `galaxy-resolver`). - **`mxaccess-asb-nettcp`** — `[MS-NMF]` framing + `[MC-NBFX]` binary-XML + `[MC-NBFS]` static dictionary + DH/HMAC/AES auth crypto with constant-time `mod_exp` via `crypto-bigint::DynResidue` (F27). - **`mxaccess-asb`** — `IASBIDataV2` client (`Connect`, `RegisterItems`, `Read`, `Write`, `PublishWriteComplete`, `CreateSubscription`, `AddMonitoredItems`, `Publish`, `DeleteMonitoredItems`, `DeleteSubscription`, `Disconnect`) with canonical-XML HMAC signing for all 13 `ConnectedRequest` shapes (F28) and DataContract field-suffix names on the binary `MonitoredItem` body (F34). - **`mxaccess`** — async Tokio façade exposing `Session`, `AsbSession`, `Subscription` (`Stream>`), `subscribe_buffered` per R2 single-sample-with-cadence-knob semantics (F36), `recover_connection` reconnect loop (F16), recovery events (`RecoveryEvent::Started/Recovered/Failed`), and a typed `Error` taxonomy. Recovery replay re-issues `RegisterReference` (not `AdviseSupervisory`) for buffered subscriptions so the `.property(buffer)` shape survives transport rebuild (F45); `unsubscribe` skips the `UnAdvise` wire frame for buffered subscriptions to match the .NET reference's `IsBuffered` guard (F47). `Session::ensure_publisher_connected` issues the `INmxService2::Connect` + `AddSubscriberEngine` round-trip before the first advise against each publishing engine, so `0x33` DataUpdate frames flow on this AVEVA install (F56). New `WriteHandle { correlation_id }` returned by `*_with_handle` write variants for per-operation correlation; `OperationStatus.context` carries the originating `OperationContext` (F54). Optional `metrics` feature emits per-op counters, latency histograms, and connection-state gauges (F40). - **`mxaccess-compat`** — `LMXProxyServer`-shaped Rust facade exposing the 18-method `ILMXProxyServer5` surface as async fns over `mxaccess::Session` / `AsbSession` with a `Mutex>` handle table and `Stream`-based event channels (F35). `LmxClient` spawns an `operation_status_drain` fan-out task that routes `Write` / `WriteSecured` events to `on_write_complete` and every other op kind to `on_operation_complete`, dropping events with unknown correlation ids silently (F54). - **Examples** — `connect-write-read.rs`, `subscribe.rs`, `subscribe-buffered.rs`, `asb-subscribe.rs`, `multi-tag.rs`, `recovery.rs`, `secured-write.rs`, plus diagnostic `asb-relay.rs`. Live-probe DoD verified end-to-end against the AVEVA install. - **Tooling** — `cargo public-api` baselines under `design/public-api/{crate}.txt` with CI drift check (F41). `design/M6-bench-baseline.md` records the alloc-count baseline. - **Performance (post-baseline) — F52.** Three codec optimisations measured against the F38 alloc-count harness: - `write_message::encode_to_bytes_mut` (F52.1) — `BytesMut` output so consumers can `split_to` / `freeze` and forward to a wire-level sink without copying. Same alloc count as `encode`. - Thread-local name-signature cache (F52.2) — repeated `MxReferenceHandle::from_names` calls with the same names skip the `to_lowercase` + CRC walk. `from_names` drops 2 → 0 allocs/op once warm; bounded at 1024 entries per thread. - `write_message::encode_into_bytes_mut` (F52.3) — caller-supplied `BytesMut` scratch buffer; reusing across writes drops fixed-width scalars from 2 → 1 alloc/op and Boolean from 1 → 0. Bench deltas pinned in `design/M6-bench-baseline.md` § F52.{1,2,3}. - **Live evidence — F49 / F50 / F51.** F49 step 5 (`LmxClient` `OnWriteComplete` round-trip) verified live against AVEVA via `cargo test -p mxaccess-compat --features live-windows-com --test lmx_write_complete_live`. F50 captured `Suspend` (NMX opcode `0x2D`, server-side) + `Activate` (client-side, no wire traffic) under `captures/123-frida-suspend-advised-instrumented/` + `captures/124-frida-activate-advised-instrumented/`; R5 settled. F51 provisioned 7 UDAs on `$TestMachine` via `wwtools/graccesscli` (TestFloat / TestDouble / TestDateTime / TestDuration + array variants), captured live `AsbVariant` wire bytes for each scalar type, and pinned them via `crates/mxaccess-codec/tests/f51_type_matrix_parity.rs`. - **`MxStatus` synthesizer kernel** — Path A from `Lmx.dll` `FUN_10100ce0` ported into `MxStatus::from_packed_u32`. Settles R3 + R4 (`OperationComplete` trigger conditions and completion-only byte mappings: the .NET reference's `WriteCompleted` is itself a half-implementation; the Rust port preserves the wire bytes verbatim and routes them through the synthesizer kernel). ### Changed (vs the .NET reference) - `NmxSubscriptionMessage::parse_data_update` accepts `record_count >= 1`; the .NET reference hard-throws on `record_count != 1`. F44 evidence walk against `captures/094-frida-buffered-separate-writer/` documents the multi-record observation that drove the divergence. - `subscribe_buffered` returns a `Stream` (single-sample-per-event); per R2 verification the cadence is a server-side delivery rate knob, not a multi-sample payload. ### Known limitations - **F3** — cross-domain NTLM Type1/2/3 fixture is permanently out-of-scope on the dev host (single-domain only). Single-domain wire parity is verified; cross-domain rounds-trip through the same shape-agnostic AV-pair codec but no live fixture pins it. Self- contained provisioning recipe (lab topology, capture procedure, fixture layout, round-trip test skeleton) at `docs/F3-cross-domain-ntlm-recipe.md` for whoever has access to a two-forest Windows lab. - **F53 (protocol crates only)** — `#![warn(missing_docs)]` is enabled and warning-clean on the consumer-facing `mxaccess` + `mxaccess-compat` lib roots. Protocol crates measure 1883 missing-docs warnings (mostly struct-field-level wire-shape records); enabling the lint there would add per-field one-liners without consumer value. Lint stays off on protocol crates indefinitely. Per-module `#![allow(missing_docs)]` opt-out is the re-introduction path if a contributor wants per-crate enforcement. ## Publish order > **Note (2026-05-06, F48):** the workspace will not be published to > crates.io. Internal usage only; consumers depend via path or git. > The dependency DAG below is retained as a workspace-hygiene check > (`design/F48-publish-dry-run.md` validates each crate's `cargo > package --list` produces a clean tarball with no accidental > captures or large files) and as the publish recipe if the policy > ever changes (e.g. an internal contributor wants registry-style > versioning via a private cargo registry). Workspace crates form a dependency DAG; `cargo publish` requires already-published deps to exist on crates.io, so the order matters: 1. `mxaccess-codec` (no internal deps) 2. `mxaccess-rpc` (no internal deps) 3. `mxaccess-asb-nettcp` (no internal deps) 4. `mxaccess-galaxy` (depends on codec) 5. `mxaccess-callback` (depends on rpc + codec) 6. `mxaccess-asb` (depends on codec + asb-nettcp) 7. `mxaccess-nmx` (depends on codec + galaxy + rpc + callback) 8. `mxaccess` (depends on all the above) 9. `mxaccess-compat` (depends on mxaccess) `cargo publish --dry-run` validates each crate's metadata + tarball in isolation; the dependent crates' dry-runs require the leaf crates to actually exist on crates.io (the registry lookup happens regardless of `--no-verify`). For pre-publish verification: leaf crates dry-run in CI; dependent crates are validated by the public-api baseline + build-test-clippy matrix.