diff --git a/src/AR.Iec61850/Scl/Export/AuthoritativeLiveIedSclExporter.cs b/src/AR.Iec61850/Scl/Export/AuthoritativeLiveIedSclExporter.cs
new file mode 100644
index 0000000..499a99a
--- /dev/null
+++ b/src/AR.Iec61850/Scl/Export/AuthoritativeLiveIedSclExporter.cs
@@ -0,0 +1,156 @@
+using System.Xml.Linq;
+using AR.Iec61850.Discovery;
+
+namespace AR.Iec61850.Scl.Export;
+
+///
+/// Exports a live-discovery model while keeping the physical IED identity separate from
+/// communication-level MMS Logical Device domain names.
+///
+public static class AuthoritativeLiveIedSclExporter
+{
+ private static readonly XNamespace Scl = "http://www.iec.ch/61850/2003/SCL";
+
+ public static LiveIedSclExportResult WriteFiles(
+ LiveIedModelDiscoveryDocument model,
+ string sclPath,
+ LiveIedSclExportOptions? options = null)
+ {
+ ArgumentNullException.ThrowIfNull(model);
+ options ??= new LiveIedSclExportOptions();
+
+ var result = LiveIedSclExporter.WriteFiles(model, sclPath, options);
+ if (string.IsNullOrWhiteSpace(options.IedNameOverride))
+ return result;
+
+ var document = XDocument.Load(result.SclPath, LoadOptions.PreserveWhitespace);
+ ApplyIdentity(document, model, options.IedNameOverride);
+ document.Save(result.SclPath);
+ return result;
+ }
+
+ public static XDocument ApplyIdentity(
+ XDocument source,
+ LiveIedModelDiscoveryDocument model,
+ string authoritativeIedName)
+ {
+ ArgumentNullException.ThrowIfNull(source);
+ ArgumentNullException.ThrowIfNull(model);
+ if (string.IsNullOrWhiteSpace(authoritativeIedName))
+ throw new ArgumentException("Authoritative IED name is empty.", nameof(authoritativeIedName));
+
+ var document = new XDocument(source);
+ var root = document.Root ?? throw new InvalidDataException("Generated SCL document has no root element.");
+ var safeIedName = SafeXmlName(authoritativeIedName);
+
+ var ied = root.Elements(Scl + "IED").SingleOrDefault()
+ ?? throw new InvalidDataException("Generated live SCL must contain exactly one IED element.");
+ ied.SetAttributeValue("name", safeIedName);
+
+ foreach (var connectedAp in root.Descendants(Scl + "ConnectedAP"))
+ connectedAp.SetAttributeValue("iedName", safeIedName);
+
+ var header = root.Element(Scl + "Header");
+ header?.SetAttributeValue("id", $"{safeIedName}_GENERATED");
+
+ var logicalDevices = ied.Descendants(Scl + "LDevice").ToArray();
+ var unmatchedDomains = new HashSet(
+ model.LogicalDevices
+ .Select(LogicalDeviceDomain)
+ .Where(domain => !string.IsNullOrWhiteSpace(domain)),
+ StringComparer.OrdinalIgnoreCase);
+
+ foreach (var logicalDevice in logicalDevices)
+ {
+ var inst = ((string?)logicalDevice.Attribute("inst") ?? string.Empty).Trim();
+ if (string.IsNullOrWhiteSpace(inst))
+ continue;
+
+ var domain = MatchMmsDomain(inst, model.IedName, unmatchedDomains);
+ if (string.IsNullOrWhiteSpace(domain))
+ continue;
+
+ unmatchedDomains.Remove(domain);
+ var implicitName = $"{safeIedName}{inst}";
+ logicalDevice.SetAttributeValue(
+ "ldName",
+ domain.Equals(implicitName, StringComparison.OrdinalIgnoreCase) ? null : domain);
+ }
+
+ ValidateIdentity(document, safeIedName, model);
+ return document;
+ }
+
+ private static string MatchMmsDomain(
+ string generatedInst,
+ string previousIedName,
+ IReadOnlySet domains)
+ {
+ var direct = domains.FirstOrDefault(domain =>
+ domain.Equals(generatedInst, StringComparison.OrdinalIgnoreCase));
+ if (!string.IsNullOrWhiteSpace(direct))
+ return direct;
+
+ var previousImplicit = $"{previousIedName?.Trim()}{generatedInst}";
+ var implicitMatch = domains.FirstOrDefault(domain =>
+ domain.Equals(previousImplicit, StringComparison.OrdinalIgnoreCase));
+ if (!string.IsNullOrWhiteSpace(implicitMatch))
+ return implicitMatch;
+
+ return string.Empty;
+ }
+
+ private static void ValidateIdentity(
+ XDocument document,
+ string authoritativeIedName,
+ LiveIedModelDiscoveryDocument model)
+ {
+ var root = document.Root ?? throw new InvalidDataException("Normalized SCL has no root element.");
+ var ied = root.Elements(Scl + "IED").Single();
+ if (!authoritativeIedName.Equals((string?)ied.Attribute("name"), StringComparison.Ordinal))
+ throw new InvalidDataException("Normalized SCL IED identity does not match the authoritative IED name.");
+
+ if (root.Descendants(Scl + "ConnectedAP").Any(ap =>
+ !authoritativeIedName.Equals((string?)ap.Attribute("iedName"), StringComparison.Ordinal)))
+ {
+ throw new InvalidDataException("ConnectedAP identity is inconsistent with the normalized IED name.");
+ }
+
+ var exportedCommunicationNames = root.Descendants(Scl + "LDevice")
+ .Select(ld =>
+ {
+ var explicitName = ((string?)ld.Attribute("ldName") ?? string.Empty).Trim();
+ var inst = ((string?)ld.Attribute("inst") ?? string.Empty).Trim();
+ return string.IsNullOrWhiteSpace(explicitName)
+ ? $"{authoritativeIedName}{inst}"
+ : explicitName;
+ })
+ .ToHashSet(StringComparer.OrdinalIgnoreCase);
+
+ var missingDomains = model.LogicalDevices
+ .Select(LogicalDeviceDomain)
+ .Where(domain => !exportedCommunicationNames.Contains(domain))
+ .ToArray();
+ if (missingDomains.Length > 0)
+ {
+ throw new InvalidDataException(
+ $"Normalized SCL lost MMS Logical Device domain(s): {string.Join(", ", missingDomains)}.");
+ }
+ }
+
+ private static string LogicalDeviceDomain(LiveIedLogicalDeviceModel logicalDevice)
+ => string.IsNullOrWhiteSpace(logicalDevice.MmsDomain)
+ ? logicalDevice.Inst.Trim()
+ : logicalDevice.MmsDomain.Trim();
+
+ private static string SafeXmlName(string value)
+ {
+ var chars = value.Trim()
+ .Select(character => char.IsLetterOrDigit(character) || character is '_' or '-' ? character : '_')
+ .ToArray();
+ var result = new string(chars);
+ if (string.IsNullOrWhiteSpace(result))
+ return "LIVE_IED";
+ return char.IsLetter(result[0]) || result[0] == '_' ? result : $"_{result}";
+ }
+}
diff --git a/src/AR.Iec61850/Scl/Export/LiveIedSclExportModels.cs b/src/AR.Iec61850/Scl/Export/LiveIedSclExportModels.cs
index 3a233df..69dff6a 100644
--- a/src/AR.Iec61850/Scl/Export/LiveIedSclExportModels.cs
+++ b/src/AR.Iec61850/Scl/Export/LiveIedSclExportModels.cs
@@ -9,6 +9,12 @@ public sealed class LiveIedSclExportOptions
public SclSchemaProfile SchemaProfile { get; init; } = SclSchemaProfile.Edition2V31;
public string SubNetworkName { get; init; } = "StationBus";
public string IpAddress { get; init; } = string.Empty;
+ ///
+ /// Authoritative SCL IED identity supplied by the caller. MMS exposes Logical Device
+ /// domain names, which are not a reliable source for recovering the enclosing IED name
+ /// when an implementation uses explicit communication-level LD names.
+ ///
+ public string IedNameOverride { get; init; } = string.Empty;
public string IpSubnet { get; init; } = "255.255.255.0";
public string IpGateway { get; init; } = "0.0.0.0";
public string OsiApTitle { get; init; } = string.Empty;
diff --git a/tests/AR.Iec61850.Tests/Scl/LiveIedSclIedIdentityTests.cs b/tests/AR.Iec61850.Tests/Scl/LiveIedSclIedIdentityTests.cs
new file mode 100644
index 0000000..a89c62f
--- /dev/null
+++ b/tests/AR.Iec61850.Tests/Scl/LiveIedSclIedIdentityTests.cs
@@ -0,0 +1,134 @@
+using AR.Iec61850.Discovery;
+using AR.Iec61850.Scl.Export;
+using System.Xml.Linq;
+
+namespace AR.Iec61850.Tests.Scl;
+
+public sealed class LiveIedSclIedIdentityTests
+{
+ [Fact]
+ public void Exporter_Uses_Authoritative_Ied_Name_And_Preserves_Explicit_Mms_Domains()
+ {
+ var model = new LiveIedModelDiscoveryDocument
+ {
+ Host = "1.110.1.1",
+ // Reproduces the unsafe legacy heuristic: the first MMS domain was copied as IED name.
+ IedName = "OCR7SJ8Application",
+ AccessPointName = "AP1",
+ LogicalDevices =
+ [
+ LogicalDevice("OCR7SJ8Application"),
+ LogicalDevice("OCR7SJ8CB1")
+ ],
+ DataSets =
+ [
+ new LiveIedDataSetModel
+ {
+ Reference = "OCR7SJ8Application/LLN0.DataSet",
+ Domain = "OCR7SJ8Application",
+ LogicalNode = "LLN0",
+ Name = "DataSet",
+ MemberCount = 1,
+ Members =
+ [
+ new LiveIedDataSetMemberModel
+ {
+ Index = 0,
+ Reference = "OCR7SJ8CB1/XCBR1.Pos.stVal",
+ FunctionalConstraint = "ST"
+ }
+ ]
+ }
+ ],
+ ReportControls =
+ [
+ new LiveIedReportControlModel
+ {
+ Reference = "OCR7SJ8Application/LLN0.RP.urcbD0101",
+ Domain = "OCR7SJ8Application",
+ LogicalNode = "LLN0",
+ Name = "urcbD0101",
+ Buffered = false,
+ DataSetReference = "OCR7SJ8Application/LLN0.DataSet",
+ ConfRev = "1"
+ }
+ ]
+ };
+
+ var generated = LiveIedSclExporter.BuildDocument(
+ model,
+ new LiveIedSclExportOptions
+ {
+ Profile = "full-model",
+ SchemaProfile = SclSchemaProfile.Edition1V16,
+ IpAddress = "1.110.1.1"
+ });
+ var document = AuthoritativeLiveIedSclExporter.ApplyIdentity(
+ generated,
+ model,
+ "OCR7SJ8Mod2");
+
+ var root = Assert.IsType(document.Root);
+ var ns = root.Name.Namespace;
+ var ied = Assert.Single(root.Elements(ns + "IED"));
+ Assert.Equal("OCR7SJ8Mod2", (string?)ied.Attribute("name"));
+ Assert.Equal("OCR7SJ8Mod2", (string?)root.Descendants(ns + "ConnectedAP").Single().Attribute("iedName"));
+
+ var logicalDevices = root.Descendants(ns + "LDevice").ToArray();
+ Assert.Equal(2, logicalDevices.Length);
+ Assert.Contains(logicalDevices, ld =>
+ (string?)ld.Attribute("inst") == "OCR7SJ8Application" &&
+ (string?)ld.Attribute("ldName") == "OCR7SJ8Application");
+ Assert.Contains(logicalDevices, ld =>
+ (string?)ld.Attribute("inst") == "OCR7SJ8CB1" &&
+ (string?)ld.Attribute("ldName") == "OCR7SJ8CB1");
+
+ var report = Assert.Single(root.Descendants(ns + "ReportControl"));
+ Assert.Equal("urcbD0101", (string?)report.Attribute("name"));
+ var fcda = Assert.Single(root.Descendants(ns + "FCDA"));
+ Assert.Equal("OCR7SJ8CB1", (string?)fcda.Attribute("ldInst"));
+ Assert.DoesNotContain("OCR7SJ8Mod2OCR7SJ8Application", document.ToString(), StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void Exporter_Keeps_Implicit_Product_Naming_When_Domain_Starts_With_Ied_Name()
+ {
+ var model = new LiveIedModelDiscoveryDocument
+ {
+ IedName = "IED1",
+ LogicalDevices = [LogicalDevice("IED1PROT")]
+ };
+
+ var generated = LiveIedSclExporter.BuildDocument(model, new LiveIedSclExportOptions());
+ var document = AuthoritativeLiveIedSclExporter.ApplyIdentity(generated, model, "IED1");
+ var root = Assert.IsType(document.Root);
+ var ns = root.Name.Namespace;
+ var logicalDevice = Assert.Single(root.Descendants(ns + "LDevice"));
+
+ Assert.Equal("PROT", (string?)logicalDevice.Attribute("inst"));
+ Assert.Null(logicalDevice.Attribute("ldName"));
+ }
+
+ private static LiveIedLogicalDeviceModel LogicalDevice(string domain)
+ => new()
+ {
+ MmsDomain = domain,
+ Inst = domain,
+ LogicalNodes =
+ [
+ new LiveIedLogicalNodeModel
+ {
+ Name = "LLN0",
+ LnClass = "LLN0",
+ ProposedLnTypeId = $"LN_{domain}_LLN0"
+ },
+ new LiveIedLogicalNodeModel
+ {
+ Name = "XCBR1",
+ LnClass = "XCBR",
+ LnInst = "1",
+ ProposedLnTypeId = $"LN_{domain}_XCBR1"
+ }
+ ]
+ };
+}