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 f2a05e27..baa6b73e 100644
--- a/archreview/plans/R2-11-tagconfig-consolidation-plan.md.tasks.json
+++ b/archreview/plans/R2-11-tagconfig-consolidation-plan.md.tasks.json
@@ -12,7 +12,7 @@
{ "id": "T8", "subject": "DraftValidator swap: Configuration→Commons ProjectReference; ExtractTagConfigFullName → TagConfigIntent.ExplicitFullName (null-on-absent preserved)", "status": "completed", "blockedBy": ["T2", "T3"] },
{ "id": "T9", "subject": "EquipmentNodeWalker.ExtractFullName delegates to TagConfigIntent; repoint tree-wide canonical-parser doc cites", "status": "completed", "blockedBy": ["T2", "T3"] },
{ "id": "T10", "subject": "Retire OpcUaServer.Tests ExtractTag*Tests (authority moved to Commons.Tests); verify zero 'MUST parse identically' hits", "status": "completed", "blockedBy": ["T3", "T5"] },
- { "id": "T11", "subject": "TagConfigJson strict-capable readers in Core.Abstractions (TryReadEnum Absent/Valid/Invalid, ReadEnumOrDefault, ReadWritable, DescribeInvalidEnum) (TDD)", "status": "pending", "blockedBy": [] },
+ { "id": "T11", "subject": "TagConfigJson strict-capable readers in Core.Abstractions (TryReadEnum Absent/Valid/Invalid, ReadEnumOrDefault, ReadWritable, DescribeInvalidEnum) (TDD)", "status": "completed", "blockedBy": [] },
{ "id": "T12", "subject": "Modbus equipment-tag parser: freeze test, shared readers, honour writable key, Inspect warnings", "status": "pending", "blockedBy": ["T11"] },
{ "id": "T13", "subject": "S7 equipment-tag parser: freeze test, shared readers, honour writable key, Inspect warnings (+ amend Contracts banner)", "status": "pending", "blockedBy": ["T11"] },
{ "id": "T14", "subject": "AbCip equipment-tag parser: freeze test, shared readers (writable already honoured), Inspect warnings", "status": "pending", "blockedBy": ["T11"] },
diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/TagConfigJson.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/TagConfigJson.cs
new file mode 100644
index 00000000..950b08a9
--- /dev/null
+++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/TagConfigJson.cs
@@ -0,0 +1,100 @@
+using System.Text.Json;
+
+namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+
+/// The outcome of reading an enum-valued field from a TagConfig JSON object.
+public enum JsonEnumRead
+{
+ /// The property is absent, or present but not a JSON string (nothing to validate).
+ Absent,
+
+ /// The property is a JSON string that parses (case-insensitively) to the enum.
+ Valid,
+
+ /// The property is a JSON string that does NOT parse to the enum (a typo'd value).
+ Invalid,
+}
+
+///
+/// Shared TagConfig JSON field readers for the six equipment-tag parsers (05/CONV-2). Replaces the
+/// six byte-identical private ReadEnum copies with one strict-capable home beside
+/// .
+/// Unknown-key handling is a deliberate non-goal. The TagConfig blob is a SHARED
+/// namespace — platform intent keys (FullName, alarm, isHistorized, …) coexist
+/// with per-driver address keys (region, dataType, …) and forward-compat keys the typed
+/// editors preserve. There is no single canonical schema, so these readers read the known fields and
+/// ignore the rest; they NEVER warn on unknown keys.
+///
+public static class TagConfigJson
+{
+ ///
+ /// Reads an enum-valued string field, distinguishing absent / valid / present-but-invalid so callers
+ /// can be lenient (fall back) while still reporting the invalid case. A property that is
+ /// absent or present-but-non-string yields (there is no enum
+ /// string to validate); a present JSON string that parses case-insensitively yields
+ /// ; a present JSON string that does not parse yields
+ /// .
+ ///
+ /// The enum type to parse.
+ /// The TagConfig root object.
+ /// The property name to read.
+ /// The parsed enum on ; otherwise default.
+ /// The read outcome.
+ public static JsonEnumRead TryReadEnum(JsonElement o, string name, out TEnum value)
+ where TEnum : struct, Enum
+ {
+ if (!o.TryGetProperty(name, out var e) || e.ValueKind != JsonValueKind.String)
+ {
+ value = default;
+ return JsonEnumRead.Absent;
+ }
+ if (Enum.TryParse(e.GetString(), ignoreCase: true, out value))
+ return JsonEnumRead.Valid;
+ value = default;
+ return JsonEnumRead.Invalid;
+ }
+
+ ///
+ /// Lenient enum read with today's exact semantics: absent OR invalid ⇒ ;
+ /// only a present, parseable string yields the parsed value. Bit-for-bit equivalent to the six
+ /// retired private ReadEnum copies.
+ ///
+ /// The enum type to parse.
+ /// The TagConfig root object.
+ /// The property name to read.
+ /// The value returned when the field is absent or invalid.
+ /// The parsed enum, or .
+ public static TEnum ReadEnumOrDefault(JsonElement o, string name, TEnum fallback)
+ where TEnum : struct, Enum
+ => TryReadEnum(o, name, out var v) == JsonEnumRead.Valid ? v : fallback;
+
+ ///
+ /// Reads the optional "writable" flag with AbCip semantics (the model): an explicit JSON
+ /// false ⇒ ; anything else (absent, true, or a non-bool token)
+ /// ⇒ .
+ ///
+ /// The TagConfig root object.
+ /// The value returned unless the field is an explicit JSON false.
+ /// only for an explicit false; otherwise .
+ public static bool ReadWritable(JsonElement o, bool defaultValue = true)
+ => o.TryGetProperty("writable", out var e) && e.ValueKind == JsonValueKind.False
+ ? false
+ : defaultValue;
+
+ ///
+ /// A human-readable warning for a present-but-invalid enum field (naming the field, the offending
+ /// value, and the valid enum members), or when the field is absent or valid.
+ ///
+ /// The enum type expected.
+ /// The TagConfig root object.
+ /// The property name to describe.
+ /// The warning text, or .
+ public static string? DescribeInvalidEnum(JsonElement o, string name)
+ where TEnum : struct, Enum
+ {
+ if (TryReadEnum(o, name, out _) != JsonEnumRead.Invalid) return null;
+ var raw = o.TryGetProperty(name, out var e) ? e.GetString() : null;
+ var valid = string.Join(", ", Enum.GetNames());
+ return $"value '{raw}' for '{name}' is not a valid {typeof(TEnum).Name}; valid: {valid}";
+ }
+}
diff --git a/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/TagConfigJsonTests.cs b/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/TagConfigJsonTests.cs
new file mode 100644
index 00000000..b748551b
--- /dev/null
+++ b/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/TagConfigJsonTests.cs
@@ -0,0 +1,79 @@
+using System.Text.Json;
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+
+namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests;
+
+///
+/// Pins the shared strict-capable TagConfig field readers (R2-11, 05/CONV-2): the
+/// Absent/Valid/Invalid matrix (case-insensitive),
+/// lenient equivalence to the retired copies,
+/// explicit-false-only semantics, and
+/// messages.
+///
+public sealed class TagConfigJsonTests
+{
+ /// Sample enum for the reader matrix (public so it can be an [InlineData] arg).
+ public enum Sample { Alpha, Beta, Gamma }
+
+ private static JsonElement Root(string json) => JsonDocument.Parse(json).RootElement;
+
+ [Theory]
+ [InlineData("{\"k\":\"Beta\"}", JsonEnumRead.Valid, Sample.Beta)]
+ [InlineData("{\"k\":\"beta\"}", JsonEnumRead.Valid, Sample.Beta)] // case-insensitive
+ [InlineData("{\"k\":\"GAMMA\"}", JsonEnumRead.Valid, Sample.Gamma)]
+ [InlineData("{\"k\":\"Deltaa\"}", JsonEnumRead.Invalid, default(Sample))] // typo → Invalid
+ [InlineData("{}", JsonEnumRead.Absent, default(Sample))] // absent
+ [InlineData("{\"k\":123}", JsonEnumRead.Absent, default(Sample))] // non-string → Absent (lenient)
+ [InlineData("{\"k\":null}", JsonEnumRead.Absent, default(Sample))]
+ public void TryReadEnum_matrix(string json, JsonEnumRead expected, Sample expectedValue)
+ {
+ var outcome = TagConfigJson.TryReadEnum(Root(json), "k", out var v);
+ outcome.ShouldBe(expected);
+ v.ShouldBe(expectedValue);
+ }
+
+ [Theory]
+ [InlineData("{\"k\":\"Gamma\"}", Sample.Gamma)] // valid → parsed
+ [InlineData("{\"k\":\"typo\"}", Sample.Alpha)] // invalid → fallback
+ [InlineData("{}", Sample.Alpha)] // absent → fallback
+ [InlineData("{\"k\":42}", Sample.Alpha)] // non-string → fallback
+ public void ReadEnumOrDefault_is_lenient(string json, Sample expected)
+ {
+ TagConfigJson.ReadEnumOrDefault(Root(json), "k", Sample.Alpha).ShouldBe(expected);
+ }
+
+ [Theory]
+ [InlineData("{\"writable\":false}", true, false)] // explicit false → false
+ [InlineData("{\"writable\":true}", true, true)] // explicit true → default
+ [InlineData("{}", true, true)] // absent → default
+ [InlineData("{\"writable\":\"no\"}", true, true)] // non-bool → default
+ [InlineData("{}", false, false)] // absent, default false
+ [InlineData("{\"writable\":false}", false, false)] // explicit false, default false
+ public void ReadWritable_explicit_false_only(string json, bool dflt, bool expected)
+ {
+ TagConfigJson.ReadWritable(Root(json), dflt).ShouldBe(expected);
+ }
+
+ [Fact]
+ public void DescribeInvalidEnum_names_field_value_and_valid_members()
+ {
+ var msg = TagConfigJson.DescribeInvalidEnum(Root("{\"dataType\":\"Betaa\"}"), "dataType");
+ msg.ShouldNotBeNull();
+ msg.ShouldContain("Betaa");
+ msg.ShouldContain("dataType");
+ msg.ShouldContain("Alpha");
+ msg.ShouldContain("Beta");
+ msg.ShouldContain("Gamma");
+ }
+
+ [Theory]
+ [InlineData("{\"dataType\":\"Beta\"}")] // valid → null
+ [InlineData("{}")] // absent → null
+ [InlineData("{\"dataType\":5}")] // non-string → null
+ public void DescribeInvalidEnum_null_when_absent_or_valid(string json)
+ {
+ TagConfigJson.DescribeInvalidEnum(Root(json), "dataType").ShouldBeNull();
+ }
+}