diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/EquipmentTagRefResolver.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/EquipmentTagRefResolver.cs
new file mode 100644
index 00000000..ca71667c
--- /dev/null
+++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/EquipmentTagRefResolver.cs
@@ -0,0 +1,46 @@
+using System.Collections.Concurrent;
+
+namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+
+///
+/// Resolves a driver subscription/read/write fullReference to a driver tag-definition,
+/// bridging the two authoring models: a legacy authored tag-table entry (looked up by name)
+/// OR an equipment tag whose reference is its raw TagConfig JSON (parsed on first use
+/// and cached). Negative results are cached too, so a genuinely-unknown reference is parsed once.
+///
+/// The driver's internal tag-definition type.
+public sealed class EquipmentTagRefResolver where TDef : class
+{
+ private readonly Func _byName;
+ private readonly Func _parseRef;
+ private readonly ConcurrentDictionary _cache = new(StringComparer.Ordinal);
+
+ /// Authored tag-table lookup (returns null on miss).
+ /// Parses an equipment-tag reference (TagConfig JSON) into a transient def, or null.
+ public EquipmentTagRefResolver(Func byName, Func parseRef)
+ {
+ ArgumentNullException.ThrowIfNull(byName);
+ ArgumentNullException.ThrowIfNull(parseRef);
+ _byName = byName;
+ _parseRef = parseRef;
+ }
+
+ /// True when resolves to a def (authored or equipment).
+ /// The wire reference handed to the driver.
+ /// The resolved tag-definition when this returns true.
+ /// when a definition was found.
+ public bool TryResolve(string fullReference, out TDef def)
+ {
+ var authored = _byName(fullReference);
+ if (authored is not null) { def = authored; return true; }
+
+ var resolved = _cache.GetOrAdd(fullReference, _parseRef);
+ if (resolved is not null) { def = resolved; return true; }
+
+ def = null!;
+ return false;
+ }
+
+ /// Drops the transient-parse cache (call on driver reinitialise so a config change re-parses).
+ public void Clear() => _cache.Clear();
+}
diff --git a/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/EquipmentTagRefResolverTests.cs b/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/EquipmentTagRefResolverTests.cs
new file mode 100644
index 00000000..d6b5f498
--- /dev/null
+++ b/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/EquipmentTagRefResolverTests.cs
@@ -0,0 +1,54 @@
+using Shouldly;
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+using Xunit;
+
+namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests;
+
+public class EquipmentTagRefResolverTests
+{
+ private sealed record Def(string Id);
+
+ private static EquipmentTagRefResolver Make(
+ Dictionary byName, Func parse)
+ => new(r => byName.TryGetValue(r, out var d) ? d : null, parse);
+
+ [Fact]
+ public void Legacy_name_resolves_via_byName()
+ {
+ var r = Make(new() { ["Temp"] = new Def("Temp") }, _ => null);
+ r.TryResolve("Temp", out var def).ShouldBeTrue();
+ def!.Id.ShouldBe("Temp");
+ }
+
+ [Fact]
+ public void Equipment_ref_resolves_via_parse_and_is_cached()
+ {
+ var calls = 0;
+ var r = Make(new(), reference => { calls++; return reference.StartsWith("{") ? new Def(reference) : null; });
+ r.TryResolve("{\"a\":1}", out var d1).ShouldBeTrue();
+ r.TryResolve("{\"a\":1}", out var d2).ShouldBeTrue();
+ d1!.Id.ShouldBe("{\"a\":1}");
+ calls.ShouldBe(1);
+ }
+
+ [Fact]
+ public void Garbage_ref_returns_false_and_caches_negative()
+ {
+ var calls = 0;
+ var r = Make(new(), _ => { calls++; return null; });
+ r.TryResolve("nope", out _).ShouldBeFalse();
+ r.TryResolve("nope", out _).ShouldBeFalse();
+ calls.ShouldBe(1);
+ }
+
+ [Fact]
+ public void Clear_drops_transient_cache()
+ {
+ var calls = 0;
+ var r = Make(new(), reference => { calls++; return new Def(reference); });
+ r.TryResolve("{\"a\":1}", out _).ShouldBeTrue();
+ r.Clear();
+ r.TryResolve("{\"a\":1}", out _).ShouldBeTrue();
+ calls.ShouldBe(2);
+ }
+}