diff --git a/src/AR.Iec61850/Mms/MmsG26FieldCapabilityStableRuntimePlanner.cs b/src/AR.Iec61850/Mms/MmsG26FieldCapabilityStableRuntimePlanner.cs
new file mode 100644
index 00000000..bc2119b4
--- /dev/null
+++ b/src/AR.Iec61850/Mms/MmsG26FieldCapabilityStableRuntimePlanner.cs
@@ -0,0 +1,206 @@
+using System.Security.Cryptography;
+using System.Text;
+using AR.Iec61850.Acse;
+using AR.Iec61850.Discovery;
+
+namespace AR.Iec61850.Mms;
+
+///
+/// P1.6 execution-stable wrapper around the field-capability runtime planner.
+///
+/// The generic hybrid planner numbers temporary DataSets by plan order. That is sufficient
+/// for one planning pass, but ARSAS deliberately revalidates one RCB immediately before each
+/// write. Isolating the second dynamic group would otherwise renumber it back to AR_HYB_01,
+/// which can collide with an already-active first group in the same logical device.
+///
+/// This wrapper changes only temporary dynamic DataSet identity: every dynamic RCB receives
+/// a deterministic association-independent AR_HYB_ name derived from its exact RCB
+/// reference. Member scope, RCB choice, capability gates, limits and ProductionEligible
+/// semantics remain owned by MmsGuardedDynamicReportFieldCapabilityRuntimePlanner.
+///
+public static class MmsGuardedDynamicReportFieldCapabilityStableRuntimePlanner
+{
+ public static MmsCapabilityAwareHybridReportAcquisitionPlan Build(
+ Iec61850SignalCatalogDocument catalog,
+ IEnumerable requestedSignals,
+ MmsReportInventory inventory,
+ MmsRcbAvailabilityResult availability,
+ MmsIedModelDirectory liveDirectory,
+ AcseMmsNegotiatedCapabilities? negotiatedCapabilities,
+ MmsHybridReportAcquisitionOptions? options,
+ MmsDynamicReportGuardedRuntimePlanningContext sourceContext,
+ MmsDynamicReportLegacyDataChangeCompatibilityEvidence evidence)
+ {
+ var plan = MmsGuardedDynamicReportFieldCapabilityRuntimePlanner.Build(
+ catalog,
+ requestedSignals,
+ inventory,
+ availability,
+ liveDirectory,
+ negotiatedCapabilities,
+ options,
+ sourceContext,
+ evidence);
+
+ return WithStableDynamicDataSetIdentities(plan);
+ }
+
+ internal static MmsCapabilityAwareHybridReportAcquisitionPlan WithStableDynamicDataSetIdentities(
+ MmsCapabilityAwareHybridReportAcquisitionPlan source)
+ {
+ ArgumentNullException.ThrowIfNull(source);
+
+ var replacements = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ var segments = source.AcquisitionPlan.Segments
+ .Select(segment => StabilizeSegment(segment, replacements))
+ .ToArray();
+
+ if (replacements.Count == 0)
+ return source;
+
+ var assignments = source.AcquisitionPlan.Assignments
+ .Select(assignment => StabilizeAssignment(assignment, replacements))
+ .ToArray();
+
+ var acquisition = new MmsHybridReportAcquisitionPlan
+ {
+ SchemaVersion = source.AcquisitionPlan.SchemaVersion,
+ Status = source.AcquisitionPlan.Status,
+ Capability = source.AcquisitionPlan.Capability,
+ Segments = segments,
+ Assignments = assignments,
+ Warnings = source.AcquisitionPlan.Warnings,
+ Blockers = source.AcquisitionPlan.Blockers
+ };
+
+ return new MmsCapabilityAwareHybridReportAcquisitionPlan
+ {
+ AcquisitionPlan = acquisition,
+ AssociationCapability = source.AssociationCapability,
+ AutomaticDynamicActivationQuarantined = source.AutomaticDynamicActivationQuarantined,
+ ProductionDynamicActivationAuthorized = source.ProductionDynamicActivationAuthorized,
+ ProductionDynamicAuthorizationReason = source.ProductionDynamicAuthorizationReason,
+ ProductionQualifiedDynamicMemberCount = source.ProductionQualifiedDynamicMemberCount,
+ ProductionQualifiedRcbReference = source.ProductionQualifiedRcbReference,
+ PolicyWarnings = source.PolicyWarnings
+ };
+ }
+
+ private static MmsHybridAcquisitionSegment StabilizeSegment(
+ MmsHybridAcquisitionSegment segment,
+ IDictionary replacements)
+ {
+ if (segment.Kind is not (MmsHybridAcquisitionKind.DynamicBrcb or MmsHybridAcquisitionKind.DynamicUrcb) ||
+ segment.ReportPlan is null ||
+ string.IsNullOrWhiteSpace(segment.ReportControlReference) ||
+ string.IsNullOrWhiteSpace(segment.ReportPlan.DataSetReference))
+ {
+ return segment;
+ }
+
+ var oldReference = segment.ReportPlan.DataSetReference;
+ var newReference = BuildStableDataSetReference(oldReference, segment.ReportControlReference);
+ if (oldReference.Equals(newReference, StringComparison.OrdinalIgnoreCase))
+ return segment;
+
+ replacements[ReplacementKey(segment.ReportControlReference, oldReference)] = newReference;
+ var reportPlan = CloneReportPlan(segment.ReportPlan, oldReference, newReference);
+
+ return new MmsHybridAcquisitionSegment
+ {
+ Kind = segment.Kind,
+ Activation = segment.Activation,
+ ReportPlan = reportPlan,
+ Availability = segment.Availability,
+ Signals = segment.Signals,
+ RequiresWrite = segment.RequiresWrite,
+ IsAlreadyActiveByCaller = segment.IsAlreadyActiveByCaller,
+ Reason = segment.Reason
+ };
+ }
+
+ private static MmsReportSubscriptionPlan CloneReportPlan(
+ MmsReportSubscriptionPlan source,
+ string oldDataSetReference,
+ string newDataSetReference)
+ => new()
+ {
+ Mode = source.Mode,
+ Status = source.Status,
+ ReportControl = source.ReportControl,
+ DataSetReference = newDataSetReference,
+ Members = source.Members,
+ DynamicPoints = source.DynamicPoints,
+ Steps = source.Steps
+ .Select(step => ReplaceOrdinalIgnoreCase(step, oldDataSetReference, newDataSetReference))
+ .ToArray(),
+ Warnings = source.Warnings,
+ Blockers = source.Blockers,
+ RcbSelection = source.RcbSelection
+ };
+
+ private static MmsHybridSignalAssignment StabilizeAssignment(
+ MmsHybridSignalAssignment assignment,
+ IReadOnlyDictionary replacements)
+ {
+ if (assignment.Kind is not (MmsHybridAcquisitionKind.DynamicBrcb or MmsHybridAcquisitionKind.DynamicUrcb) ||
+ string.IsNullOrWhiteSpace(assignment.ReportControlReference) ||
+ string.IsNullOrWhiteSpace(assignment.DataSetReference))
+ {
+ return assignment;
+ }
+
+ if (!replacements.TryGetValue(
+ ReplacementKey(assignment.ReportControlReference, assignment.DataSetReference),
+ out var replacement))
+ {
+ return assignment;
+ }
+
+ return new MmsHybridSignalAssignment
+ {
+ SignalReference = assignment.SignalReference,
+ Kind = assignment.Kind,
+ ReportControlReference = assignment.ReportControlReference,
+ DataSetReference = replacement,
+ IsReportBacked = assignment.IsReportBacked,
+ Reason = assignment.Reason
+ };
+ }
+
+ private static string BuildStableDataSetReference(string currentDataSetReference, string rcbReference)
+ {
+ var current = (currentDataSetReference ?? string.Empty).Trim().Replace('\\', '/');
+ var separator = current.LastIndexOf('.');
+ if (separator < 0)
+ return current;
+
+ return current[..(separator + 1)] + BuildStableDataSetName(rcbReference);
+ }
+
+ internal static string BuildStableDataSetName(string rcbReference)
+ {
+ var normalized = MmsRcbAvailabilityEvaluator.NormalizeReference(rcbReference)
+ .Trim()
+ .Replace('\\', '/')
+ .ToUpperInvariant();
+ var digest = SHA256.HashData(Encoding.UTF8.GetBytes(normalized));
+ return "AR_HYB_" + Convert.ToHexString(digest)[..12];
+ }
+
+ private static string ReplacementKey(string rcbReference, string dataSetReference)
+ => MmsRcbAvailabilityEvaluator.NormalizeReference(rcbReference).Replace('\\', '/') + "\n" +
+ (dataSetReference ?? string.Empty).Trim().Replace('\\', '/');
+
+ private static string ReplaceOrdinalIgnoreCase(string source, string oldValue, string newValue)
+ {
+ if (string.IsNullOrEmpty(source) || string.IsNullOrEmpty(oldValue))
+ return source;
+
+ var index = source.IndexOf(oldValue, StringComparison.OrdinalIgnoreCase);
+ if (index < 0)
+ return source;
+
+ return source[..index] + newValue + source[(index + oldValue.Length)..];
+ }
+}
diff --git a/tests/AR.Iec61850.Tests/Mms/MmsG26FieldCapabilityStableRuntimePlannerTests.cs b/tests/AR.Iec61850.Tests/Mms/MmsG26FieldCapabilityStableRuntimePlannerTests.cs
new file mode 100644
index 00000000..27ba22ec
--- /dev/null
+++ b/tests/AR.Iec61850.Tests/Mms/MmsG26FieldCapabilityStableRuntimePlannerTests.cs
@@ -0,0 +1,150 @@
+using AR.Iec61850.Mms;
+
+namespace AR.Iec61850.Tests.Mms;
+
+public sealed class MmsG26FieldCapabilityStableRuntimePlannerTests
+{
+ [Fact]
+ public void SameRcb_KeepsSameDynamicDataSetIdentity_AcrossFullAndIsolatedRevalidation()
+ {
+ var full = MmsGuardedDynamicReportFieldCapabilityStableRuntimePlanner.WithStableDynamicDataSetIdentities(
+ CapabilityPlan(
+ DynamicSegment("LD0/LLN0.RP.U01", "LD0/LLN0.AR_HYB_01", "LD0/GGIO1$ST$Ind1$stVal"),
+ DynamicSegment("LD0/LLN0.RP.U02", "LD0/LLN0.AR_HYB_02", "LD0/GGIO1$ST$Ind2$stVal")));
+
+ var isolatedSecond = MmsGuardedDynamicReportFieldCapabilityStableRuntimePlanner.WithStableDynamicDataSetIdentities(
+ CapabilityPlan(
+ DynamicSegment("LD0/LLN0.RP.U02", "LD0/LLN0.AR_HYB_01", "LD0/GGIO1$ST$Ind2$stVal")));
+
+ var fullSegments = full.AcquisitionPlan.Segments.ToArray();
+ var fullFirst = fullSegments[0].DataSetReference;
+ var fullSecond = fullSegments[1].DataSetReference;
+ var isolated = Assert.Single(isolatedSecond.AcquisitionPlan.Segments).DataSetReference;
+
+ Assert.NotEqual(fullFirst, fullSecond);
+ Assert.Equal(fullSecond, isolated);
+ Assert.StartsWith("LD0/LLN0.AR_HYB_", fullSecond, StringComparison.OrdinalIgnoreCase);
+ Assert.False(fullSecond.EndsWith("_01", StringComparison.OrdinalIgnoreCase));
+ Assert.False(fullSecond.EndsWith("_02", StringComparison.OrdinalIgnoreCase));
+ }
+
+ [Fact]
+ public void StableIdentity_ChangesOnlyDynamicDataSetReference_NotRcbOrMembersOrProductionBoundary()
+ {
+ var source = CapabilityPlan(
+ DynamicSegment("LD0/LLN0.RP.U01", "LD0/LLN0.AR_HYB_01", "LD0/GGIO1$ST$Ind1$stVal"));
+ var result = MmsGuardedDynamicReportFieldCapabilityStableRuntimePlanner.WithStableDynamicDataSetIdentities(source);
+
+ var before = Assert.Single(source.AcquisitionPlan.Segments);
+ var after = Assert.Single(result.AcquisitionPlan.Segments);
+
+ Assert.Equal(before.ReportControlReference, after.ReportControlReference);
+ Assert.Equal(before.ReportPlan!.DynamicPoints.Select(point => point.MmsReference),
+ after.ReportPlan!.DynamicPoints.Select(point => point.MmsReference));
+ Assert.NotEqual(before.DataSetReference, after.DataSetReference);
+ Assert.False(result.ProductionDynamicActivationAuthorized);
+ Assert.Equal(source.ProductionDynamicAuthorizationReason, result.ProductionDynamicAuthorizationReason);
+ }
+
+ [Fact]
+ public void StaticSegment_IsReturnedUnchanged()
+ {
+ var staticSegment = new MmsHybridAcquisitionSegment
+ {
+ Kind = MmsHybridAcquisitionKind.StaticUrcb,
+ Activation = MmsHybridReportActivation.EnableExistingDataSet,
+ ReportPlan = new MmsReportSubscriptionPlan
+ {
+ Mode = MmsReportSubscriptionPlanMode.StaticDataSet,
+ Status = MmsReportSubscriptionPlanStatus.ReadyRequiresWrite,
+ ReportControl = Candidate("LD0/LLN0.RP.Static01"),
+ DataSetReference = "LD0/LLN0.StaticData"
+ }
+ };
+ var source = CapabilityPlan(staticSegment);
+ var result = MmsGuardedDynamicReportFieldCapabilityStableRuntimePlanner.WithStableDynamicDataSetIdentities(source);
+
+ Assert.Same(source, result);
+ }
+
+ private static MmsCapabilityAwareHybridReportAcquisitionPlan CapabilityPlan(params MmsHybridAcquisitionSegment[] segments)
+ => new()
+ {
+ AcquisitionPlan = new MmsHybridReportAcquisitionPlan
+ {
+ Status = MmsHybridAcquisitionPlanStatus.FullReportCoverage,
+ Segments = segments,
+ Assignments = segments.SelectMany(segment => segment.Signals.Select(signal => new MmsHybridSignalAssignment
+ {
+ SignalReference = signal.EffectiveMmsReference,
+ Kind = segment.Kind,
+ ReportControlReference = segment.ReportControlReference,
+ DataSetReference = segment.DataSetReference,
+ IsReportBacked = true,
+ Reason = "fixture"
+ })).ToArray()
+ },
+ ProductionDynamicActivationAuthorized = false,
+ ProductionDynamicAuthorizationReason = "ProductionEligible remains separate."
+ };
+
+ private static MmsHybridAcquisitionSegment DynamicSegment(string rcb, string dataSet, string member)
+ {
+ var point = Point(member);
+ var signal = new AR.Iec61850.Discovery.Iec61850SignalDescriptor
+ {
+ EffectiveMmsReference = member,
+ CanonicalMmsReference = member,
+ FunctionalConstraint = "ST",
+ SemanticRole = AR.Iec61850.Discovery.Iec61850DataAttributeSemanticRole.PrimaryValue,
+ IsOperationalCandidate = true
+ };
+ return new MmsHybridAcquisitionSegment
+ {
+ Kind = MmsHybridAcquisitionKind.DynamicUrcb,
+ Activation = MmsHybridReportActivation.ConfigureDynamicDataSet,
+ Signals = [signal],
+ RequiresWrite = true,
+ ReportPlan = new MmsReportSubscriptionPlan
+ {
+ Mode = MmsReportSubscriptionPlanMode.DynamicDataSet,
+ Status = MmsReportSubscriptionPlanStatus.ReadyRequiresWrite,
+ ReportControl = Candidate(rcb),
+ DataSetReference = dataSet,
+ DynamicPoints = [point],
+ Members = [new MmsDataSetDirectoryMember { Domain = point.Domain, MmsItemName = point.MmsItemName }],
+ Steps = [$"Create dynamic DataSet {dataSet}."]
+ }
+ };
+ }
+
+ private static MmsReportControlCandidate Candidate(string reference)
+ => new()
+ {
+ Domain = reference.Split('/')[0],
+ LogicalNode = "LLN0",
+ FunctionalConstraint = "RP",
+ Name = reference.Split('.').Last(),
+ Reference = reference,
+ Buffered = false,
+ EnabledState = "false",
+ ReservationState = "false"
+ };
+
+ private static MmsFcResolvedPoint Point(string member)
+ {
+ var slash = member.IndexOf('/');
+ var domain = member[..slash];
+ var item = member[(slash + 1)..];
+ return new MmsFcResolvedPoint
+ {
+ Domain = domain,
+ LogicalNode = "GGIO1",
+ FunctionalConstraint = "ST",
+ DataObjectPath = "Ind.stVal",
+ MmsItemName = item,
+ Source = "fixture",
+ Confidence = 100
+ };
+ }
+}