diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/DataConnections/OpcUaReferenceForm.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/DataConnections/OpcUaReferenceForm.cs
new file mode 100644
index 00000000..b4c963d5
--- /dev/null
+++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/DataConnections/OpcUaReferenceForm.cs
@@ -0,0 +1,55 @@
+using System.Globalization;
+
+namespace ZB.MOM.WW.ScadaBridge.Commons.Types.DataConnections;
+
+///
+/// Pure, protocol-package-free classification of an OPC UA node-reference string's
+/// form — used by authoring UI to nudge operators toward index-durable bindings.
+///
+///
+/// A bare ns=<index>;… reference hard-codes a namespace index, which is
+/// only meaningful relative to one server's namespace table. The OtOpcUa v3.0 raw/uns
+/// split makes this actively dangerous: v2's sole custom namespace and v3's raw
+/// tree both land at ns=2, so a stored ns=2;s=… resolves without error but
+/// silently now means a raw-tree node. The durable form is nsu=<uri>;…,
+/// resolved against the live namespace table at use time by
+/// OpcUaNodeReference.Resolve. This helper only classifies the string; it does not
+/// parse or resolve NodeIds (that stays in the DataConnectionLayer seam, which owns the
+/// Opc.Ua dependency).
+///
+public static class OpcUaReferenceForm
+{
+ ///
+ /// True when is index-durable — the durable
+ /// nsu=<uri>;… form, spec-fixed ns=0, a short form with no namespace
+ /// prefix, or empty/whitespace (nothing to warn about). False only for a bare
+ /// server-namespace index (ns=<n>;… with n ≥ 1).
+ ///
+ public static bool IsDurable(string? reference)
+ {
+ if (string.IsNullOrWhiteSpace(reference))
+ return true;
+
+ var trimmed = reference.Trim();
+
+ // nsu=;… is the durable form (and its "ns" prefix must be checked before the
+ // bare "ns=" branch below, since "nsu=" also starts with "ns").
+ if (trimmed.StartsWith("nsu=", StringComparison.OrdinalIgnoreCase))
+ return true;
+
+ // Not a bare namespace-index reference at all → short forms (i=85, s=Foo) that
+ // imply namespace 0, which is spec-fixed and already durable.
+ if (!trimmed.StartsWith("ns=", StringComparison.OrdinalIgnoreCase))
+ return true;
+
+ // ns=;… — parse the index; ns=0 is spec-fixed (durable), n ≥ 1 is bare.
+ var semicolon = trimmed.IndexOf(';');
+ var digits = semicolon >= 0 ? trimmed[3..semicolon] : trimmed[3..];
+ if (int.TryParse(digits, NumberStyles.None, CultureInfo.InvariantCulture, out var index))
+ return index == 0;
+
+ // Malformed "ns=" with non-numeric index — not a recognisable bare index; leave
+ // the real parse/reject to OpcUaNodeReference.Resolve. Don't warn on it here.
+ return true;
+ }
+}
diff --git a/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/DataConnections/OpcUaReferenceFormTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/DataConnections/OpcUaReferenceFormTests.cs
new file mode 100644
index 00000000..63fb12f7
--- /dev/null
+++ b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/DataConnections/OpcUaReferenceFormTests.cs
@@ -0,0 +1,27 @@
+using ZB.MOM.WW.ScadaBridge.Commons.Types.DataConnections;
+
+namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types.DataConnections;
+
+public class OpcUaReferenceFormTests
+{
+ [Theory]
+ // durable → true
+ [InlineData("nsu=https://zb.com/otopcua/raw;s=Line1.Pump.Speed", true)]
+ [InlineData("nsu=https://zb.com/otopcua/uns;s=AreaA/Line1/Pump/Speed", true)]
+ [InlineData("NSU=https://x;s=y", true)] // case-insensitive prefix
+ [InlineData("ns=0;i=85", true)] // spec-fixed namespace 0
+ [InlineData("i=85", true)] // short form, implicit ns0
+ [InlineData("s=Devices.Pump1", true)] // no namespace prefix
+ [InlineData("", true)] // nothing typed
+ [InlineData(" ", true)]
+ [InlineData(null, true)]
+ // bare server-namespace index → false (warn)
+ [InlineData("ns=2;s=MyDevice.Temperature", false)]
+ [InlineData("ns=1;i=1001", false)]
+ [InlineData("ns=10;s=x", false)]
+ [InlineData(" ns=3;s=x ", false)] // trimmed before inspection
+ public void IsDurable_classifies_reference_forms(string? reference, bool expected)
+ {
+ Assert.Equal(expected, OpcUaReferenceForm.IsDurable(reference));
+ }
+}