diff --git a/ZB.MOM.WW.OtOpcUa.slnx b/ZB.MOM.WW.OtOpcUa.slnx
index f9170dfc..85b032b2 100644
--- a/ZB.MOM.WW.OtOpcUa.slnx
+++ b/ZB.MOM.WW.OtOpcUa.slnx
@@ -48,6 +48,7 @@
+
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.csproj b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.csproj
index 83eccb18..88d6e667 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.csproj
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.csproj
@@ -8,6 +8,10 @@
+
+
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts/OpcUaClientDriverOptions.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts/OpcUaClientDriverOptions.cs
index d1bc2f08..47c27525 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts/OpcUaClientDriverOptions.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts/OpcUaClientDriverOptions.cs
@@ -31,7 +31,8 @@ public sealed record OpcUaClientDriverOptions
/// with the cluster's raw OPC UA Client tags. Each carries the tag's
/// RawPath identity plus a driver TagConfig blob whose nodeId key holds the
/// upstream OPC UA node id string the driver dials. The driver builds a RawPath → nodeId
- /// resolution table (via ) from this list at connect time; a
+ /// resolution table (via NamespaceMap, in the sibling .Session project) from this
+ /// list at connect time; a
/// read/write/subscribe/history reference is always a RawPath resolved through that table.
///
public IReadOnlyList RawTags { get; init; } = [];
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts/OpcUaClientTagDefinitionFactory.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts/OpcUaClientTagDefinitionFactory.cs
index 6f35ab84..7a63848a 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts/OpcUaClientTagDefinitionFactory.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts/OpcUaClientTagDefinitionFactory.cs
@@ -4,13 +4,19 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient;
///
/// The authored-raw-tag reader for the OPC UA Client (gateway) driver: one authority, consumed by both
-/// the runtime ('s RawPath → upstream node id table build) and the
+/// the runtime (NamespaceMap's RawPath → upstream node id table build) and the
/// deploy-time TagConfig gate (, deferment-register G-7).
///
-/// This read used to be private to . It moved here — unchanged —
+/// This read used to be private to NamespaceMap. It moved here — unchanged —
/// rather than being mirrored: a mirror of an accept/reject boundary drifts the moment either side
/// is touched, and the drift is silent, with the gate describing a shape the driver no longer
-/// reads. now calls .
+/// reads. NamespaceMap now calls .
+///
+///
+/// NamespaceMap is named by text, not cref, on purpose. It lives in the
+/// sibling Driver.OpcUaClient.Session project, which references THIS one — the dependency
+/// runs that way round so this assembly stays dependency-light for ControlPlane, which
+/// project-references it for the G-7 gate. A cref here would need the reverse reference.
///
///
/// is never stricter than — it asks the
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts.csproj b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts.csproj
index aa67cc0d..d547a460 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts.csproj
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts.csproj
@@ -6,11 +6,12 @@
true
-
-
-
-
+
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts/NamespaceMap.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Session/NamespaceMap.cs
similarity index 94%
rename from src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts/NamespaceMap.cs
rename to src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Session/NamespaceMap.cs
index d7371d86..a71dc1c0 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts/NamespaceMap.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Session/NamespaceMap.cs
@@ -52,6 +52,17 @@ public readonly record struct RawTagBinding(string NodeId, bool WriteIdempotent)
/// driver then re-binds against the live session via the static
/// .
///
+///
+/// Why this type sits in .Session and not .Contracts. Its whole public
+/// surface is OPC UA SDK-typed (, ,
+/// ), so while it lived in .Contracts that project had to carry
+/// OPCFoundation.NetStandard.Opc.Ua.Client — and once ControlPlane project-referenced
+/// .Contracts for the G-7 deploy-time TagConfig gate, the whole OPC UA client stack
+/// came with it into every host that loads the control plane. The blob read this class needs
+/// stayed behind in .Contracts as
+/// OpcUaClientTagDefinitionFactory.TryReadNodeId (the ONE authority the deploy gate and
+/// this table share), which is why the reference runs .Session → .Contracts.
+///
///
public sealed class NamespaceMap
{
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Session/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Session.csproj b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Session/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Session.csproj
new file mode 100644
index 00000000..3ecc9663
--- /dev/null
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Session/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Session.csproj
@@ -0,0 +1,46 @@
+
+
+ net10.0
+ enable
+ enable
+ true
+
+ ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient
+ ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Session
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.csproj b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.csproj
index 0226d2fe..7c2369d4 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.csproj
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.csproj
@@ -14,6 +14,9 @@
+
+
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/ZB.MOM.WW.OtOpcUa.ControlPlane.csproj b/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/ZB.MOM.WW.OtOpcUa.ControlPlane.csproj
index e4f43208..36fedc39 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/ZB.MOM.WW.OtOpcUa.ControlPlane.csproj
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/ZB.MOM.WW.OtOpcUa.ControlPlane.csproj
@@ -54,10 +54,8 @@
TagConfigValidator checked them. Adding the reference is what makes
EquipmentTagConfigInspectorCoverageTests demand a dispatch-map entry for them — the guard scans
the output directory, so an unreferenced Contracts assembly is invisible to it.
- Note OpcUaClient.Contracts brings OPCFoundation.NetStandard.Opc.Ua.Client transitively: its
- NamespaceMap re-binds a stable nsu= reference against a live session. Every host that loads
- ControlPlane (Host, AdminUI) already carries the OPC UA stack, so this adds no new deployment
- surface. -->
+ OpcUaClient.Contracts is dependency-light (Core.Abstractions only) — the OPC UA client SDK it
+ used to pull in transitively left with NamespaceMap, to Driver.OpcUaClient.Session. -->
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests.csproj b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests.csproj
index 84c25286..d2bb2cfd 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests.csproj
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests.csproj
@@ -22,7 +22,9 @@
-
+
+
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/ContractsIsDependencyLightTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/ContractsIsDependencyLightTests.cs
new file mode 100644
index 00000000..9cf5de8a
--- /dev/null
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/ContractsIsDependencyLightTests.cs
@@ -0,0 +1,74 @@
+using Shouldly;
+using Xunit;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests;
+
+///
+/// Pins the dependency shape of ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts: it must not
+/// reference the OPC UA client SDK.
+///
+/// Why this matters beyond tidiness. Deferment-register G-7 wired
+/// into the deploy-time TagConfig gate, which
+/// made ControlPlane project-reference this Contracts assembly. ControlPlane is loaded by
+/// every admin- and driver-role host, so whatever Contracts drags in, they all carry. Contracts
+/// used to hold NamespaceMap — ISession/NodeId-typed throughout — and with it a
+/// OPCFoundation.NetStandard.Opc.Ua.Client PackageReference; NamespaceMap moved to the
+/// sibling .Session leaf so the parse-only half could stay light (the same shape as
+/// Calculation.Contracts: Core.Abstractions and nothing else).
+///
+///
+/// Direction matters: this is an ABSENCE assertion, and that is what makes it sound here.
+/// The assembly manifest is an unreliable witness for PRESENCE — the C# compiler elides a reference
+/// nothing in the IL actually uses, so "assembly X is referenced" can be false while the csproj says
+/// otherwise (the trap EquipmentTagConfigInspectorCoverageTests documents, which is why that
+/// one scans the output directory instead). Absence has no such hole in the direction that matters:
+/// the moment someone adds Opc.Ua-typed code to Contracts, the compiler MUST emit the reference and
+/// this test goes red. A csproj PackageReference that nothing consumes is elided and slips past —
+/// accepted deliberately, because an unused package reference is not the failure being guarded.
+///
+///
+/// Falsified 2026-08-09 by temporarily adding an Opc.Ua.NodeId-typed member to Contracts:
+/// the assertion failed naming Opc.Ua.Core, and passed again once removed.
+///
+///
+public sealed class ContractsIsDependencyLightTests
+{
+ /// The Contracts assembly's manifest must name no OPC UA SDK assembly.
+ [Fact]
+ public void The_Contracts_assembly_does_not_reference_the_OPC_UA_SDK()
+ {
+ var contracts = typeof(OpcUaClientTagDefinitionFactory).Assembly;
+
+ // Positive control: name the assembly under test, so a rename/retarget cannot silently point this
+ // at something else that trivially satisfies the assertion below.
+ contracts.GetName().Name.ShouldBe("ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts");
+
+ var opcUa = contracts.GetReferencedAssemblies()
+ .Select(a => a.Name ?? string.Empty)
+ .Where(n => n.StartsWith("Opc.Ua", StringComparison.OrdinalIgnoreCase))
+ .OrderBy(n => n, StringComparer.Ordinal)
+ .ToArray();
+
+ opcUa.ShouldBeEmpty(
+ "OpcUaClient.Contracts must stay free of the OPC UA client SDK — ControlPlane project-references "
+ + "it for the G-7 deploy-time TagConfig gate, so every host that loads the control plane would "
+ + "carry the stack. Put SDK-typed code in Driver.OpcUaClient.Session instead. Found: "
+ + string.Join(", ", opcUa));
+ }
+
+ ///
+ /// The counterpart the assertion above is meaningful against: the SDK-typed half really did land
+ /// somewhere, and that somewhere does reference the SDK. Without this, "no Opc.Ua reference
+ /// anywhere" would read as a pass.
+ ///
+ [Fact]
+ public void The_Session_assembly_is_where_the_OPC_UA_SDK_reference_lives()
+ {
+ var session = typeof(NamespaceMap).Assembly;
+
+ session.GetName().Name.ShouldBe("ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Session");
+ session.GetReferencedAssemblies()
+ .Select(a => a.Name ?? string.Empty)
+ .ShouldContain(n => n.StartsWith("Opc.Ua", StringComparison.OrdinalIgnoreCase));
+ }
+}