diff --git a/archreview/plans/R2-11-tagconfig-consolidation-plan.md.tasks.json b/archreview/plans/R2-11-tagconfig-consolidation-plan.md.tasks.json
index e014d4c4..297e1971 100644
--- a/archreview/plans/R2-11-tagconfig-consolidation-plan.md.tasks.json
+++ b/archreview/plans/R2-11-tagconfig-consolidation-plan.md.tasks.json
@@ -21,7 +21,7 @@
{ "id": "T17", "subject": "FOCAS equipment-tag parser: force Writable:false (05/UNDER-1 correction), shared readers, Inspect warnings (+ amend Contracts banner)", "status": "completed", "blockedBy": ["T11"] },
{ "id": "T18", "subject": "FOCAS capability-matrix pre-flight on the equipment-tag _parseRef resolve path (BadNodeIdUnknown on rejection)", "status": "completed", "blockedBy": ["T17"] },
{ "id": "T19", "subject": "Modbus probe parses the factory DTO shape (timeoutMs round-trip; OpcUaClient parity rule)", "status": "completed", "blockedBy": [] },
- { "id": "T20", "subject": "EquipmentTagConfigInspector DriverType-dispatch map in ControlPlane (+6 Contracts references)", "status": "pending", "blockedBy": ["T12", "T13", "T14", "T15", "T16", "T17"] },
+ { "id": "T20", "subject": "EquipmentTagConfigInspector DriverType-dispatch map in ControlPlane (+6 Contracts references)", "status": "completed", "blockedBy": ["T12", "T13", "T14", "T15", "T16", "T17"] },
{ "id": "T21", "subject": "AdminOperationsActor deploy-gate wiring + Deployment:TagConfigValidationMode (Warn default | Error opt-in); actor-level consuming test", "status": "pending", "blockedBy": ["T20"] },
{ "id": "T22", "subject": "AdminUI writable checkbox in the six driver-typed tag editors (models + razor; live /run verify at execution)", "status": "pending", "blockedBy": [] },
{ "id": "T23", "subject": "Docs + decision record: two-mode rollout, unknown-keys non-goal, FOCAS writable migration note, STATUS.md/00-INDEX.md entries + Phase-C follow-up", "status": "pending", "blockedBy": ["T17", "T21"] },
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/AdminOperations/EquipmentTagConfigInspector.cs b/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/AdminOperations/EquipmentTagConfigInspector.cs
new file mode 100644
index 00000000..852ec7c9
--- /dev/null
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/AdminOperations/EquipmentTagConfigInspector.cs
@@ -0,0 +1,47 @@
+using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
+using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
+using ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
+using ZB.MOM.WW.OtOpcUa.Driver.Modbus;
+using ZB.MOM.WW.OtOpcUa.Driver.S7;
+using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
+
+namespace ZB.MOM.WW.OtOpcUa.ControlPlane.AdminOperations;
+
+///
+/// Deploy-time TagConfig inspection dispatched by DriverType (R2-11, 05/CONV-2). Mirrors the
+/// AdminUI TagConfigValidator shape: a case-insensitive map from the canonical driver-type string
+/// to that driver's lightweight Contracts parser Inspect(), returning human-readable
+/// warnings (present-but-invalid enum values that the lenient runtime silently defaults, plus
+/// structurally-unparseable TagConfig). Unmapped drivers (Galaxy, OpcUaClient) are skipped exactly as in
+/// the AdminUI validator. Never throws.
+///
+public static class EquipmentTagConfigInspector
+{
+ private static readonly IReadOnlyDictionary>> Inspectors =
+ new Dictionary>>(StringComparer.OrdinalIgnoreCase)
+ {
+ ["Modbus"] = ModbusEquipmentTagParser.Inspect,
+ ["S7"] = S7EquipmentTagParser.Inspect,
+ ["AbCip"] = AbCipEquipmentTagParser.Inspect,
+ ["AbLegacy"] = AbLegacyEquipmentTagParser.Inspect,
+ ["TwinCat"] = TwinCATEquipmentTagParser.Inspect,
+ ["Focas"] = FocasEquipmentTagParser.Inspect,
+ };
+
+ /// Inspects a tag's TagConfig for the given driver type. Returns the driver's warnings,
+ /// or an empty list when the driver type is unmapped (Galaxy/OpcUaClient) or the inputs are null.
+ /// The bound driver's DriverType (canonical string, case-insensitive).
+ /// The equipment tag's TagConfig JSON.
+ /// The warnings; empty when the driver is unmapped or clean.
+ public static IReadOnlyList Inspect(string? driverType, string? tagConfig)
+ {
+ if (string.IsNullOrEmpty(driverType) || tagConfig is null) return Array.Empty();
+ return Inspectors.TryGetValue(driverType, out var inspect) ? inspect(tagConfig) : Array.Empty();
+ }
+
+ /// Whether the given driver type has a mapped inspector (equipment-tag protocol drivers only).
+ /// The bound driver's DriverType.
+ /// when an inspector is registered for the driver type.
+ public static bool IsMapped(string? driverType) =>
+ !string.IsNullOrEmpty(driverType) && Inspectors.ContainsKey(driverType);
+}
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 87fe34cc..4c0ed37c 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
@@ -26,6 +26,14 @@
+
+
+
+
+
+
+
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/EquipmentTagConfigInspectorTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/EquipmentTagConfigInspectorTests.cs
new file mode 100644
index 00000000..b99a5052
--- /dev/null
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/EquipmentTagConfigInspectorTests.cs
@@ -0,0 +1,62 @@
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.ControlPlane.AdminOperations;
+
+namespace ZB.MOM.WW.OtOpcUa.ControlPlane.Tests;
+
+/// R2-11 (05/CONV-2): the DriverType-dispatched deploy-time TagConfig inspector — known drivers
+/// route to their parser's Inspect; unmapped drivers (Galaxy/OpcUaClient) skip; keys are case-insensitive.
+public sealed class EquipmentTagConfigInspectorTests
+{
+ [Fact]
+ public void Modbus_typo_dispatches_to_one_warning()
+ {
+ var warnings = EquipmentTagConfigInspector.Inspect(
+ "Modbus", "{\"region\":\"HoldingRegisters\",\"address\":1,\"dataType\":\"Intt16\"}");
+ warnings.ShouldHaveSingleItem();
+ warnings[0].ShouldContain("Intt16");
+ }
+
+ [Fact]
+ public void Clean_modbus_config_has_no_warnings()
+ {
+ EquipmentTagConfigInspector.Inspect(
+ "Modbus", "{\"region\":\"HoldingRegisters\",\"address\":1,\"dataType\":\"Int16\"}")
+ .ShouldBeEmpty();
+ }
+
+ [Theory]
+ [InlineData("GalaxyMxGateway")]
+ [InlineData("OpcUaClient")]
+ [InlineData("Unknown")]
+ public void Unmapped_driver_types_are_skipped(string driverType)
+ {
+ EquipmentTagConfigInspector.Inspect(driverType, "{\"dataType\":\"whatever\"}").ShouldBeEmpty();
+ EquipmentTagConfigInspector.IsMapped(driverType).ShouldBeFalse();
+ }
+
+ [Theory]
+ [InlineData("modbus")]
+ [InlineData("FOCAS")]
+ [InlineData("focas")]
+ [InlineData("twincat")]
+ public void Driver_type_matching_is_case_insensitive(string driverType)
+ {
+ EquipmentTagConfigInspector.IsMapped(driverType).ShouldBeTrue();
+ }
+
+ [Fact]
+ public void Focas_writable_true_dispatches_read_only_warning()
+ {
+ var warnings = EquipmentTagConfigInspector.Inspect(
+ "Focas", "{\"address\":\"D0100\",\"dataType\":\"Int32\",\"writable\":true}");
+ warnings.ShouldContain(w => w.Contains("read-only"));
+ }
+
+ [Fact]
+ public void Null_inputs_are_safe()
+ {
+ EquipmentTagConfigInspector.Inspect(null, "{}").ShouldBeEmpty();
+ EquipmentTagConfigInspector.Inspect("Modbus", null).ShouldBeEmpty();
+ }
+}