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 d952ba71..19647b38 100644
--- a/archreview/plans/R2-11-tagconfig-consolidation-plan.md.tasks.json
+++ b/archreview/plans/R2-11-tagconfig-consolidation-plan.md.tasks.json
@@ -3,7 +3,7 @@
"lastUpdated": "2026-07-12",
"tasks": [
{ "id": "T1", "subject": "Golden TagConfig corpus + compose→encode→decode parity characterization test (Runtime.Tests, green on unmodified tree)", "status": "completed", "blockedBy": [] },
- { "id": "T2", "subject": "FullName-semantics characterization for EquipmentNodeWalker (Core.Tests) + DraftValidator Galaxy rule (Configuration.Tests)", "status": "pending", "blockedBy": [] },
+ { "id": "T2", "subject": "FullName-semantics characterization for EquipmentNodeWalker (Core.Tests) + DraftValidator Galaxy rule (Configuration.Tests)", "status": "completed", "blockedBy": [] },
{ "id": "T3", "subject": "TagConfigIntent + TagAlarmIntent in Commons/Types (TDD; port composer semantics verbatim + OpcUaServer ExtractTag* test tables)", "status": "pending", "blockedBy": [] },
{ "id": "T4", "subject": "DeviceConfigIntent (TryExtractHost/NormalizeHost) in Commons/Types (TDD)", "status": "pending", "blockedBy": [] },
{ "id": "T5", "subject": "AddressSpaceComposer swap: single TagConfigIntent.Parse per tag, delete 4 Extract* statics (P-1 parse-once)", "status": "pending", "blockedBy": ["T1", "T3"] },
diff --git a/tests/Core/ZB.MOM.WW.OtOpcUa.Configuration.Tests/DraftValidatorGalaxyFullNameCorpusTests.cs b/tests/Core/ZB.MOM.WW.OtOpcUa.Configuration.Tests/DraftValidatorGalaxyFullNameCorpusTests.cs
new file mode 100644
index 00000000..e4af659e
--- /dev/null
+++ b/tests/Core/ZB.MOM.WW.OtOpcUa.Configuration.Tests/DraftValidatorGalaxyFullNameCorpusTests.cs
@@ -0,0 +1,60 @@
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
+using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
+using ZB.MOM.WW.OtOpcUa.Configuration.Validation;
+
+namespace ZB.MOM.WW.OtOpcUa.Configuration.Tests;
+
+///
+/// Characterization net (R2-11) pinning the DraftValidator Galaxy rule's explicit-FullName
+/// semantics across the golden TagConfig corpus BEFORE the private FullName reader is delegated to
+/// TagConfigIntent.Parse(...).ExplicitFullName. Distinct from the walker/composer raw-blob
+/// fallback: GalaxyTagMissingReference fires whenever the TagConfig has no usable
+/// explicit string FullName — i.e. absent / non-string / whitespace / non-object /
+/// malformed — and does NOT fire only when a non-blank string FullName is present. Must be green on
+/// the unmodified tree and stay green through the swap.
+///
+public sealed class DraftValidatorGalaxyFullNameCorpusTests
+{
+ [Theory]
+ // Present, non-blank string FullName → rule does NOT fire
+ [InlineData("{\"FullName\":\"X.Y\"}", false)]
+ [InlineData("{\"FullName\":\"A.B\",\"region\":\"Coils\"}", false)]
+ // Absent / non-string / whitespace / non-object / malformed → rule FIRES
+ [InlineData("{}", true)]
+ [InlineData("{\"region\":\"HoldingRegisters\"}", true)]
+ [InlineData("{\"FullName\":123}", true)]
+ [InlineData("{\"FullName\":\" \"}", true)]
+ [InlineData("[1,2]", true)]
+ [InlineData("not json {", true)]
+ public void Galaxy_missing_reference_fires_iff_no_explicit_string_FullName(string tagConfig, bool shouldFire)
+ {
+ var draft = new DraftSnapshot
+ {
+ GenerationId = 1,
+ ClusterId = "c",
+ DriverInstances =
+ [
+ new DriverInstance
+ {
+ DriverInstanceId = "d-galaxy", ClusterId = "c", NamespaceId = "ns-1",
+ Name = "Galaxy", DriverType = "GalaxyMxGateway", DriverConfig = "{}",
+ },
+ ],
+ Tags =
+ [
+ new Tag
+ {
+ TagId = "tag-galaxytag", DriverInstanceId = "d-galaxy", EquipmentId = "eq-1",
+ Name = "galaxytag", FolderPath = null, DataType = "Float",
+ AccessLevel = TagAccessLevel.Read, TagConfig = tagConfig,
+ },
+ ],
+ };
+
+ var fired = DraftValidator.Validate(draft)
+ .Any(e => e.Code == "GalaxyTagMissingReference" && e.Context == "tag-galaxytag");
+ fired.ShouldBe(shouldFire);
+ }
+}
diff --git a/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Tests/OpcUa/EquipmentNodeWalkerFullNameCorpusTests.cs b/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Tests/OpcUa/EquipmentNodeWalkerFullNameCorpusTests.cs
new file mode 100644
index 00000000..ae74919d
--- /dev/null
+++ b/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Tests/OpcUa/EquipmentNodeWalkerFullNameCorpusTests.cs
@@ -0,0 +1,36 @@
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Core.OpcUa;
+
+namespace ZB.MOM.WW.OtOpcUa.Core.Tests.OpcUa;
+
+///
+/// Characterization net (R2-11) pinning 's
+/// raw-blob-fallback semantics across the golden TagConfig corpus BEFORE the parser is delegated to
+/// TagConfigIntent.Parse. The walker's FullName copy falls back to the raw blob whenever the
+/// input is not a JSON object carrying a string FullName (blank / non-object / malformed /
+/// absent / non-string). Whitespace + non-string-property values are returned verbatim (not trimmed).
+/// Must be green on the unmodified tree and stay green through the swap.
+///
+public sealed class EquipmentNodeWalkerFullNameCorpusTests
+{
+ [Theory]
+ // object with a string FullName → the FullName string (verbatim, incl. whitespace)
+ [InlineData("{\"FullName\":\"X.Y\"}", "X.Y")]
+ [InlineData("{\"FullName\":\" \"}", " ")]
+ [InlineData("{\"FullName\":\"A.B\",\"region\":\"Coils\",\"address\":5}", "A.B")]
+ // blank → raw blob (IsNullOrWhiteSpace short-circuit)
+ [InlineData(" ", " ")]
+ // non-object root → raw blob
+ [InlineData("[1,2]", "[1,2]")]
+ // malformed JSON → raw blob
+ [InlineData("not json {", "not json {")]
+ // object, FullName absent → raw blob
+ [InlineData("{\"region\":\"HoldingRegisters\",\"address\":5}", "{\"region\":\"HoldingRegisters\",\"address\":5}")]
+ // object, FullName present but non-string → raw blob
+ [InlineData("{\"FullName\":123}", "{\"FullName\":123}")]
+ public void ExtractFullName_falls_back_to_raw_blob_except_for_string_FullName(string tagConfig, string expected)
+ {
+ EquipmentNodeWalker.ExtractFullName(tagConfig).ShouldBe(expected);
+ }
+}