diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Transport/BundleNameMap.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Transport/BundleNameMap.cs
new file mode 100644
index 00000000..467962b9
--- /dev/null
+++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Transport/BundleNameMap.cs
@@ -0,0 +1,53 @@
+namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Transport;
+
+///
+/// How a source-environment site or connection referenced by an incoming bundle is
+/// resolved against the destination environment during import.
+///
+public enum MappingAction
+{
+ /// Bind the source name to an existing destination site/connection
+ /// (the corresponding Target* identifier is required).
+ MapToExisting,
+
+ /// Create a new destination site/connection from the bundle's payload
+ /// (the corresponding Target* identifier is left null).
+ CreateNew,
+}
+
+///
+/// Resolves one site referenced by an incoming bundle (M8 site/instance-scoped
+/// transport). is required when
+/// is and null
+/// when it is .
+///
+public sealed record SiteMapping(
+ string SourceSiteIdentifier,
+ MappingAction Action,
+ string? TargetSiteIdentifier);
+
+///
+/// Resolves one data connection (scoped to a source site) referenced by an incoming
+/// bundle. is required when
+/// is and null
+/// when it is .
+///
+public sealed record ConnectionMapping(
+ string SourceSiteIdentifier,
+ string SourceConnectionName,
+ MappingAction Action,
+ string? TargetConnectionName);
+
+///
+/// The operator-supplied resolution of every source-environment site and connection
+/// name an incoming bundle references, applied during import. Crosses the Central UI ⇆
+/// ManagementActor boundary via System.Text.Json.
+///
+public sealed record BundleNameMap(
+ IReadOnlyList Sites,
+ IReadOnlyList Connections)
+{
+ /// An empty map (no site or connection mappings).
+ public static BundleNameMap Empty { get; } =
+ new(Array.Empty(), Array.Empty());
+}
diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Transport/BundleSummary.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Transport/BundleSummary.cs
index 328a682e..5397980e 100644
--- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Transport/BundleSummary.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Transport/BundleSummary.cs
@@ -10,4 +10,9 @@ public sealed record BundleSummary(
int DbConnections,
int NotificationLists,
int SmtpConfigs,
- int ApiMethods);
+ int ApiMethods,
+ // Additive (M8 A1): site/instance-scoped artifacts. Defaulted to 0 so manifests
+ // written by older exporters (which omit these JSON properties) deserialize fine.
+ int Sites = 0,
+ int DataConnections = 0,
+ int Instances = 0);
diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Transport/ExportSelection.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Transport/ExportSelection.cs
index b0d2d622..051e6413 100644
--- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Transport/ExportSelection.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Transport/ExportSelection.cs
@@ -13,4 +13,15 @@ public sealed record ExportSelection(
IReadOnlyList NotificationListIds,
IReadOnlyList SmtpConfigurationIds,
IReadOnlyList ApiMethodIds,
- bool IncludeDependencies);
+ bool IncludeDependencies,
+ // Additive (M8 A1): site/instance-scoped export. Defaulted to empty so every
+ // existing positional caller keeps compiling; older callers select no sites/instances.
+ IReadOnlyList? SiteIds = null,
+ IReadOnlyList? InstanceIds = null)
+{
+ /// Sites selected for site/instance-scoped export (M8). Never null.
+ public IReadOnlyList SiteIds { get; init; } = SiteIds ?? Array.Empty();
+
+ /// Instances selected for site/instance-scoped export (M8). Never null.
+ public IReadOnlyList InstanceIds { get; init; } = InstanceIds ?? Array.Empty();
+}
diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Transport/ImportPreview.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Transport/ImportPreview.cs
index 35914147..c0fa855b 100644
--- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Transport/ImportPreview.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Transport/ImportPreview.cs
@@ -11,6 +11,40 @@ public sealed record ImportPreviewItem(
string? FieldDiffJson,
string? BlockerReason);
+///
+/// A site reference in an incoming bundle that the operator must resolve before import
+/// (M8 A1). is the destination site the
+/// importer auto-matched by identifier, or null when no match was found.
+///
+public sealed record RequiredSiteMapping(
+ string SourceSiteIdentifier,
+ string SourceSiteName,
+ string? AutoMatchTargetIdentifier);
+
+///
+/// A connection reference (scoped to a source site) in an incoming bundle that the
+/// operator must resolve before import (M8 A1).
+/// is the destination connection the importer auto-matched by name, or null when no
+/// match was found.
+///
+public sealed record RequiredConnectionMapping(
+ string SourceSiteIdentifier,
+ string SourceConnectionName,
+ string? AutoMatchTargetName);
+
public sealed record ImportPreview(
Guid SessionId,
- IReadOnlyList Items);
+ IReadOnlyList Items,
+ // Additive (M8 A1): site/connection references the operator must resolve before
+ // applying. Defaulted to empty so every existing positional caller keeps compiling.
+ IReadOnlyList? RequiredSiteMappings = null,
+ IReadOnlyList? RequiredConnectionMappings = null)
+{
+ /// Site references the operator must resolve before import (M8). Never null.
+ public IReadOnlyList RequiredSiteMappings { get; init; } =
+ RequiredSiteMappings ?? Array.Empty();
+
+ /// Connection references the operator must resolve before import (M8). Never null.
+ public IReadOnlyList RequiredConnectionMappings { get; init; } =
+ RequiredConnectionMappings ?? Array.Empty();
+}
diff --git a/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/Transport/BundleNameMapTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/Transport/BundleNameMapTests.cs
new file mode 100644
index 00000000..ea12f9a9
--- /dev/null
+++ b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/Transport/BundleNameMapTests.cs
@@ -0,0 +1,127 @@
+using System.Text.Json;
+using ZB.MOM.WW.ScadaBridge.Commons.Types.Transport;
+
+namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types.Transport;
+
+///
+/// M8 A1: focused shape / value-equality / round-trip tests for the Transport (#24)
+/// name-mapping DTOs — , ,
+/// and . These records cross the Central UI ⇆ ManagementActor
+/// boundary (and may be persisted in an import session) via System.Text.Json, so a
+/// positional/tuple slip would silently mis-map sites/connections during import.
+/// Uses plain (no dependency on the Transport project's
+/// BundleJsonOptions) per the A1 task contract.
+///
+public sealed class BundleNameMapTests
+{
+ private static readonly JsonSerializerOptions JsonOpts = new(JsonSerializerDefaults.Web);
+
+ [Fact]
+ public void Empty_HasNoSitesOrConnections()
+ {
+ var map = BundleNameMap.Empty;
+
+ Assert.Empty(map.Sites);
+ Assert.Empty(map.Connections);
+ }
+
+ [Fact]
+ public void Empty_IsStableInstance()
+ {
+ // The Empty singleton must be reusable without aliasing surprises.
+ Assert.Same(BundleNameMap.Empty, BundleNameMap.Empty);
+ }
+
+ [Fact]
+ public void SiteMapping_ValueEquality_HoldsForIdenticalFields()
+ {
+ var a = new SiteMapping("site-east", MappingAction.MapToExisting, "site-1");
+ var b = new SiteMapping("site-east", MappingAction.MapToExisting, "site-1");
+
+ Assert.Equal(a, b);
+ Assert.Equal(a.GetHashCode(), b.GetHashCode());
+ }
+
+ [Fact]
+ public void SiteMapping_ValueEquality_DiffersWhenActionDiffers()
+ {
+ var mapTo = new SiteMapping("site-east", MappingAction.MapToExisting, "site-1");
+ var createNew = new SiteMapping("site-east", MappingAction.CreateNew, null);
+
+ Assert.NotEqual(mapTo, createNew);
+ }
+
+ [Fact]
+ public void ConnectionMapping_ValueEquality_HoldsForIdenticalFields()
+ {
+ var a = new ConnectionMapping("site-east", "OPC-Main", MappingAction.MapToExisting, "OPC-Primary");
+ var b = new ConnectionMapping("site-east", "OPC-Main", MappingAction.MapToExisting, "OPC-Primary");
+
+ Assert.Equal(a, b);
+ Assert.Equal(a.GetHashCode(), b.GetHashCode());
+ }
+
+ [Fact]
+ public void MapToExisting_CarriesNonNullTarget()
+ {
+ var site = new SiteMapping("site-east", MappingAction.MapToExisting, "site-1");
+ var conn = new ConnectionMapping("site-east", "OPC-Main", MappingAction.MapToExisting, "OPC-Primary");
+
+ Assert.Equal(MappingAction.MapToExisting, site.Action);
+ Assert.NotNull(site.TargetSiteIdentifier);
+ Assert.Equal("site-1", site.TargetSiteIdentifier);
+
+ Assert.Equal(MappingAction.MapToExisting, conn.Action);
+ Assert.NotNull(conn.TargetConnectionName);
+ Assert.Equal("OPC-Primary", conn.TargetConnectionName);
+ }
+
+ [Fact]
+ public void CreateNew_LeavesTargetNull()
+ {
+ var site = new SiteMapping("site-west", MappingAction.CreateNew, null);
+ var conn = new ConnectionMapping("site-west", "OPC-Aux", MappingAction.CreateNew, null);
+
+ Assert.Null(site.TargetSiteIdentifier);
+ Assert.Null(conn.TargetConnectionName);
+ }
+
+ [Fact]
+ public void BundleNameMap_JsonRoundTrip_PreservesSitesAndConnections()
+ {
+ var map = new BundleNameMap(
+ Sites: new[]
+ {
+ new SiteMapping("site-east", MappingAction.MapToExisting, "site-1"),
+ new SiteMapping("site-west", MappingAction.CreateNew, null),
+ },
+ Connections: new[]
+ {
+ new ConnectionMapping("site-east", "OPC-Main", MappingAction.MapToExisting, "OPC-Primary"),
+ new ConnectionMapping("site-west", "OPC-Aux", MappingAction.CreateNew, null),
+ });
+
+ var json = JsonSerializer.Serialize(map, JsonOpts);
+ var rt = JsonSerializer.Deserialize(json, JsonOpts);
+
+ Assert.NotNull(rt);
+ // BundleNameMap's record Equals is reference-based over its IReadOnlyList members,
+ // and JSON deserialization yields List rather than the original T[] — so the
+ // collections never reference-equal. The records *within* the lists have proper
+ // value-equality, so compare element-wise (SequenceEqual uses SiteMapping/
+ // ConnectionMapping value-equality).
+ Assert.True(map.Sites.SequenceEqual(rt!.Sites));
+ Assert.True(map.Connections.SequenceEqual(rt.Connections));
+ }
+
+ [Fact]
+ public void BundleNameMap_Empty_JsonRoundTrip_PreservesEmptiness()
+ {
+ var json = JsonSerializer.Serialize(BundleNameMap.Empty, JsonOpts);
+ var rt = JsonSerializer.Deserialize(json, JsonOpts);
+
+ Assert.NotNull(rt);
+ Assert.Empty(rt!.Sites);
+ Assert.Empty(rt.Connections);
+ }
+}