diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/AddressSpaceComposer.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/AddressSpaceComposer.cs
index 27cfbfff..72fafc22 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/AddressSpaceComposer.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/AddressSpaceComposer.cs
@@ -58,7 +58,28 @@ public sealed record AddressSpaceComposition(
public sealed record UnsAreaProjection(string UnsAreaId, string DisplayName);
public sealed record UnsLineProjection(string UnsLineId, string UnsAreaId, string DisplayName);
-public sealed record EquipmentNode(string EquipmentId, string DisplayName, string UnsLineId);
+
+/// One UNS level-5 equipment folder in the address space. is the
+/// logical NodeId; is the friendly UNS Name segment;
+/// is the parent line the folder hangs under.
+/// / carry the equipment's optional bindings
+/// (both null ⇒ driver-less / no device), copied straight from the Equipment row.
+/// is the device's connection host (e.g. "10.0.0.5:8193") resolved from the
+/// bound Device's schemaless DeviceConfig JSON via
+/// — null when there is no device, no
+/// HostAddress in its config, or the host cannot be parsed. These three let a later task graft a
+/// driver's discovered FixedTree onto an equipment that has zero authored tags, and partition a
+/// multi-device driver by host. The value is normalized identically on both the live-edit composer and
+/// the artifact-decode sides (single source of truth: );
+/// the later partition task MUST normalize the driver-discovered device-host folder segment the same way
+/// (trim + lower-case) so the two compare equal.
+public sealed record EquipmentNode(
+ string EquipmentId,
+ string DisplayName,
+ string UnsLineId,
+ string? DriverInstanceId = null,
+ string? DeviceId = null,
+ string? DeviceHost = null);
public sealed record DriverInstancePlan(string DriverInstanceId, string DriverType, string ConfigJson);
public sealed record ScriptedAlarmPlan(string ScriptedAlarmId, string EquipmentId, string PredicateScriptId, string MessageTemplate);
@@ -277,15 +298,17 @@ public static class AddressSpaceComposer
/// The equipment.
/// The driver instances.
/// The scripted alarms.
+ /// The per-device rows used to resolve each equipment's DeviceHost. null = none.
/// The composition result.
public static AddressSpaceComposition Compose(
IReadOnlyList unsAreas,
IReadOnlyList unsLines,
IReadOnlyList equipment,
IReadOnlyList driverInstances,
- IReadOnlyList scriptedAlarms) =>
+ IReadOnlyList scriptedAlarms,
+ IReadOnlyList? devices = null) =>
Compose(unsAreas, unsLines, equipment, driverInstances, scriptedAlarms,
- Array.Empty(), Array.Empty());
+ Array.Empty(), Array.Empty(), devices: devices);
///
/// Composes the address space build plan from the configuration entities.
@@ -299,6 +322,8 @@ public static class AddressSpaceComposer
/// The namespaces.
/// The Equipment-namespace virtual (calculated) tags. null = none.
/// The scripts joined to by ScriptId for the expression. null = none.
+ /// The per-device rows (DeviceId + schemaless DeviceConfig JSON) used to resolve
+ /// each equipment's DeviceHost from its bound DeviceId. null = none.
/// The composition result.
public static AddressSpaceComposition Compose(
IReadOnlyList unsAreas,
@@ -309,10 +334,19 @@ public static class AddressSpaceComposer
IReadOnlyList tags,
IReadOnlyList namespaces,
IReadOnlyList? virtualTags = null,
- IReadOnlyList