diff --git a/src/ZB.MOM.WW.OtOpcUa.Core/OpcUa/IdentificationFolderBuilder.cs b/src/ZB.MOM.WW.OtOpcUa.Core/OpcUa/IdentificationFolderBuilder.cs
new file mode 100644
index 0000000..cb28e36
--- /dev/null
+++ b/src/ZB.MOM.WW.OtOpcUa.Core/OpcUa/IdentificationFolderBuilder.cs
@@ -0,0 +1,91 @@
+using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+
+namespace ZB.MOM.WW.OtOpcUa.Core.OpcUa;
+
+///
+/// Phase 6.4 Stream D: materializes the OPC 40010 Machinery companion-spec Identification
+/// sub-folder under an Equipment node. Reads the nine decision-#139 columns off the
+/// row and emits one property per non-null field.
+///
+///
+/// Pure-function shape — testable without a real OPC UA node manager. The caller
+/// passes the builder scoped to the Equipment node; this class handles the Identification
+/// sub-folder creation + per-field calls.
+///
+/// ACL binding: the sub-folder + its properties inherit the Equipment scope's
+/// grants (no new scope level). Phase 6.2's trie treats them as part of the Equipment
+/// ScopeId — a user with Equipment-level grant reads Identification; a user without the
+/// grant gets BadUserAccessDenied on both the Equipment node + its Identification variables.
+/// See docs/v2/acl-design.md §Identification cross-reference.
+///
+/// The nine fields per decision #139 are exposed exactly when they carry a non-null
+/// value. A row with all nine null produces no Identification sub-folder at all — the
+/// caller can use to skip the Folder call entirely
+/// and avoid a pointless empty folder appearing in browse trees.
+///
+public static class IdentificationFolderBuilder
+{
+ /// Browse + display name of the sub-folder — fixed per OPC 40010 convention.
+ public const string FolderName = "Identification";
+
+ ///
+ /// Canonical decision #139 field set exposed in the Identification sub-folder. Order
+ /// matches the decision-log entry so any browse-order reader can cross-reference
+ /// without re-sorting.
+ ///
+ public static IReadOnlyList FieldNames { get; } = new[]
+ {
+ "Manufacturer", "Model", "SerialNumber",
+ "HardwareRevision", "SoftwareRevision",
+ "YearOfConstruction", "AssetLocation",
+ "ManufacturerUri", "DeviceManualUri",
+ };
+
+ /// True when the equipment row has at least one non-null Identification field.
+ public static bool HasAnyFields(Equipment equipment)
+ {
+ ArgumentNullException.ThrowIfNull(equipment);
+ return equipment.Manufacturer is not null
+ || equipment.Model is not null
+ || equipment.SerialNumber is not null
+ || equipment.HardwareRevision is not null
+ || equipment.SoftwareRevision is not null
+ || equipment.YearOfConstruction is not null
+ || equipment.AssetLocation is not null
+ || equipment.ManufacturerUri is not null
+ || equipment.DeviceManualUri is not null;
+ }
+
+ ///
+ /// Build the Identification sub-folder under . No-op
+ /// when every field is null. Returns the sub-folder builder (or null when no-op) so
+ /// callers can attach additional nodes underneath if needed.
+ ///
+ public static IAddressSpaceBuilder? Build(IAddressSpaceBuilder equipmentBuilder, Equipment equipment)
+ {
+ ArgumentNullException.ThrowIfNull(equipmentBuilder);
+ ArgumentNullException.ThrowIfNull(equipment);
+
+ if (!HasAnyFields(equipment)) return null;
+
+ var folder = equipmentBuilder.Folder(FolderName, FolderName);
+ AddIfPresent(folder, "Manufacturer", DriverDataType.String, equipment.Manufacturer);
+ AddIfPresent(folder, "Model", DriverDataType.String, equipment.Model);
+ AddIfPresent(folder, "SerialNumber", DriverDataType.String, equipment.SerialNumber);
+ AddIfPresent(folder, "HardwareRevision", DriverDataType.String, equipment.HardwareRevision);
+ AddIfPresent(folder, "SoftwareRevision", DriverDataType.String, equipment.SoftwareRevision);
+ AddIfPresent(folder, "YearOfConstruction", DriverDataType.Int32,
+ equipment.YearOfConstruction is null ? null : (object)(int)equipment.YearOfConstruction.Value);
+ AddIfPresent(folder, "AssetLocation", DriverDataType.String, equipment.AssetLocation);
+ AddIfPresent(folder, "ManufacturerUri", DriverDataType.String, equipment.ManufacturerUri);
+ AddIfPresent(folder, "DeviceManualUri", DriverDataType.String, equipment.DeviceManualUri);
+ return folder;
+ }
+
+ private static void AddIfPresent(IAddressSpaceBuilder folder, string name, DriverDataType dataType, object? value)
+ {
+ if (value is null) return;
+ folder.AddProperty(name, dataType, value);
+ }
+}
diff --git a/tests/ZB.MOM.WW.OtOpcUa.Core.Tests/OpcUa/IdentificationFolderBuilderTests.cs b/tests/ZB.MOM.WW.OtOpcUa.Core.Tests/OpcUa/IdentificationFolderBuilderTests.cs
new file mode 100644
index 0000000..d1ba349
--- /dev/null
+++ b/tests/ZB.MOM.WW.OtOpcUa.Core.Tests/OpcUa/IdentificationFolderBuilderTests.cs
@@ -0,0 +1,158 @@
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+using ZB.MOM.WW.OtOpcUa.Core.OpcUa;
+
+namespace ZB.MOM.WW.OtOpcUa.Core.Tests.OpcUa;
+
+[Trait("Category", "Unit")]
+public sealed class IdentificationFolderBuilderTests
+{
+ private sealed class RecordingBuilder : IAddressSpaceBuilder
+ {
+ public List<(string BrowseName, string DisplayName)> Folders { get; } = [];
+ public List<(string BrowseName, DriverDataType DataType, object? Value)> Properties { get; } = [];
+
+ public IAddressSpaceBuilder Folder(string browseName, string displayName)
+ {
+ Folders.Add((browseName, displayName));
+ return this; // flat recording — identification fields land in the same bucket
+ }
+
+ public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo)
+ => throw new NotSupportedException("Identification fields use AddProperty, not Variable");
+
+ public void AddProperty(string browseName, DriverDataType dataType, object? value)
+ => Properties.Add((browseName, dataType, value));
+ }
+
+ private static Equipment EmptyEquipment() => new()
+ {
+ EquipmentId = "EQ-000000000001",
+ DriverInstanceId = "drv-1",
+ UnsLineId = "line-1",
+ Name = "eq-1",
+ MachineCode = "machine_001",
+ };
+
+ private static Equipment FullyPopulatedEquipment() => new()
+ {
+ EquipmentId = "EQ-000000000001",
+ DriverInstanceId = "drv-1",
+ UnsLineId = "line-1",
+ Name = "eq-1",
+ MachineCode = "machine_001",
+ Manufacturer = "Siemens",
+ Model = "S7-1500",
+ SerialNumber = "SN-12345",
+ HardwareRevision = "Rev-A",
+ SoftwareRevision = "Fw-2.3.1",
+ YearOfConstruction = 2023,
+ AssetLocation = "Warsaw-West/Bldg-3",
+ ManufacturerUri = "https://siemens.example",
+ DeviceManualUri = "https://siemens.example/manual",
+ };
+
+ [Fact]
+ public void HasAnyFields_AllNull_ReturnsFalse()
+ {
+ IdentificationFolderBuilder.HasAnyFields(EmptyEquipment()).ShouldBeFalse();
+ }
+
+ [Fact]
+ public void HasAnyFields_OneNonNull_ReturnsTrue()
+ {
+ var eq = EmptyEquipment();
+ eq.SerialNumber = "SN-1";
+ IdentificationFolderBuilder.HasAnyFields(eq).ShouldBeTrue();
+ }
+
+ [Fact]
+ public void Build_AllNull_ReturnsNull_AndDoesNotEmit_Folder()
+ {
+ var builder = new RecordingBuilder();
+
+ var result = IdentificationFolderBuilder.Build(builder, EmptyEquipment());
+
+ result.ShouldBeNull();
+ builder.Folders.ShouldBeEmpty("no Identification folder when every field is null");
+ builder.Properties.ShouldBeEmpty();
+ }
+
+ [Fact]
+ public void Build_FullyPopulated_EmitsAllNineFields()
+ {
+ var builder = new RecordingBuilder();
+
+ var result = IdentificationFolderBuilder.Build(builder, FullyPopulatedEquipment());
+
+ result.ShouldNotBeNull();
+ builder.Folders.ShouldContain(f => f.BrowseName == "Identification");
+ builder.Properties.Count.ShouldBe(9);
+ builder.Properties.Select(p => p.BrowseName).ShouldBe(
+ ["Manufacturer", "Model", "SerialNumber",
+ "HardwareRevision", "SoftwareRevision",
+ "YearOfConstruction", "AssetLocation",
+ "ManufacturerUri", "DeviceManualUri"],
+ "property order matches decision #139 exactly");
+ }
+
+ [Fact]
+ public void Build_OnlyNonNull_Are_Emitted()
+ {
+ var eq = EmptyEquipment();
+ eq.Manufacturer = "Siemens";
+ eq.SerialNumber = "SN-1";
+ eq.YearOfConstruction = 2024;
+ var builder = new RecordingBuilder();
+
+ IdentificationFolderBuilder.Build(builder, eq);
+
+ builder.Properties.Count.ShouldBe(3, "only the 3 non-null fields are exposed");
+ builder.Properties.Select(p => p.BrowseName).ShouldBe(
+ ["Manufacturer", "SerialNumber", "YearOfConstruction"]);
+ }
+
+ [Fact]
+ public void YearOfConstruction_Maps_Short_To_Int32_DriverDataType()
+ {
+ var eq = EmptyEquipment();
+ eq.YearOfConstruction = 2023;
+ var builder = new RecordingBuilder();
+
+ IdentificationFolderBuilder.Build(builder, eq);
+
+ var prop = builder.Properties.Single(p => p.BrowseName == "YearOfConstruction");
+ prop.DataType.ShouldBe(DriverDataType.Int32);
+ prop.Value.ShouldBe(2023, "short is widened to int for OPC UA Int32 representation");
+ }
+
+ [Fact]
+ public void Build_StringValues_RoundTrip()
+ {
+ var eq = FullyPopulatedEquipment();
+ var builder = new RecordingBuilder();
+
+ IdentificationFolderBuilder.Build(builder, eq);
+
+ builder.Properties.Single(p => p.BrowseName == "Manufacturer").Value.ShouldBe("Siemens");
+ builder.Properties.Single(p => p.BrowseName == "DeviceManualUri").Value.ShouldBe("https://siemens.example/manual");
+ }
+
+ [Fact]
+ public void FieldNames_Match_Decision139_Exactly()
+ {
+ IdentificationFolderBuilder.FieldNames.ShouldBe(
+ ["Manufacturer", "Model", "SerialNumber",
+ "HardwareRevision", "SoftwareRevision",
+ "YearOfConstruction", "AssetLocation",
+ "ManufacturerUri", "DeviceManualUri"]);
+ }
+
+ [Fact]
+ public void FolderName_Is_Identification()
+ {
+ IdentificationFolderBuilder.FolderName.ShouldBe("Identification");
+ }
+}