From a2106926f3cf882a104aad046e26657f59d2009f Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 16 Aug 2026 19:45:43 +0700 Subject: [PATCH 1/4] Treat source-unbound RCBs as dynamic binding candidates --- Services/RcbExportEvidencePolicy.cs | 43 +++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/Services/RcbExportEvidencePolicy.cs b/Services/RcbExportEvidencePolicy.cs index 97d36013..74a9ab8b 100644 --- a/Services/RcbExportEvidencePolicy.cs +++ b/Services/RcbExportEvidencePolicy.cs @@ -27,22 +27,44 @@ public static string EffectiveDataSetReference( public static int EffectiveMemberCount(int liveMemberCount, int fallbackMemberCount) => liveMemberCount > 0 ? liveMemberCount : Math.Max(0, fallbackMemberCount); + /// + /// True when the source SCL intentionally/legitimately leaves the ReportControl + /// datSet association open and a successful live read shows a runtime DataSet binding. + /// This is normal dynamic-RCB evidence and must never be presented as configuration + /// mismatch merely because the source side is blank. + /// + public static bool IsDynamicRuntimeBinding( + string? sourceReference, + string? liveReference, + MmsRcbDataSetProbeState liveProbeState) + => liveProbeState == MmsRcbDataSetProbeState.ReadSucceeded && + string.IsNullOrWhiteSpace(NormalizeReference(sourceReference)) && + !string.IsNullOrWhiteSpace(NormalizeReference(liveReference)); + public static bool HasSourceLiveBindingConflict( string? sourceReference, string? liveReference, MmsRcbDataSetProbeState liveProbeState) { - // Only positive live binding evidence may contradict the source SCL. A failed or - // unattempted live read is unresolved evidence, not a configuration mismatch. + // Only positive live binding evidence may contradict a fixed source binding. + // A failed or unattempted live read is unresolved evidence, not a mismatch. if (liveProbeState != MmsRcbDataSetProbeState.ReadSucceeded) return false; var source = NormalizeReference(sourceReference); var live = NormalizeReference(liveReference); - if (source.Length == 0 && live.Length == 0) + + // An unbound source ReportControl is not a promise that the live DatSet must stay + // empty. Dynamic RCB workflows are allowed to bind a DataSet at runtime, so blank + // source + populated live binding is valid evidence rather than a conflict. + if (source.Length == 0) return false; - if (source.Length == 0 || live.Length == 0) + + // A fixed source binding *is* a contract. A successful live read proving no binding, + // or proving a different binding, is therefore a real configuration mismatch. + if (live.Length == 0) return true; + return !source.Equals(live, StringComparison.OrdinalIgnoreCase); } @@ -60,7 +82,12 @@ public static MmsRcbOperationalAvailability SourceAvailability( var hasConfiguredBinding = !string.IsNullOrWhiteSpace(configuredDataSetName); if (!hasConfiguredBinding) - return MmsRcbOperationalAvailability.NoDataSet; + { + // A blank source datSet is valid for a dynamic RCB. Before a live DatSet read + // proves the current runtime association, keep the state unknown/informational + // instead of painting the row as an operational NoDataSet failure. + return MmsRcbOperationalAvailability.Unknown; + } if (!dataSetResolved) return MmsRcbOperationalAvailability.Unknown; @@ -95,7 +122,11 @@ public static string SourceReason( bool connected) { if (string.IsNullOrWhiteSpace(configuredDataSetName)) - return "The source SCL ReportControl has no configured datSet binding."; + { + return connected + ? "The source SCL leaves the ReportControl datSet unbound. This is valid for a dynamic RCB; use Check Availability to read the current live binding." + : "The source SCL leaves the ReportControl datSet unbound. This can be a valid dynamic RCB; connect the IED to read the current live binding."; + } if (!dataSetResolved) return "The source SCL ReportControl names a DataSet, but that reference does not resolve in the same Logical Node."; From 268ef4f078b6cd39fe83d1c736f56aa42105b59e Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 16 Aug 2026 19:46:04 +0700 Subject: [PATCH 2/4] Cover dynamic RCB binding evidence semantics --- .../RcbExportEvidencePolicyTests.cs | 45 ++++++++++++++++--- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/tests/ARSAS.Tests/RcbExportEvidencePolicyTests.cs b/tests/ARSAS.Tests/RcbExportEvidencePolicyTests.cs index 319aec44..4be375ee 100644 --- a/tests/ARSAS.Tests/RcbExportEvidencePolicyTests.cs +++ b/tests/ARSAS.Tests/RcbExportEvidencePolicyTests.cs @@ -20,7 +20,7 @@ public void E016Style_ConfiguredCtrlUrcb_IsNeverNoDataSetBeforeLiveCheck() } [Fact] - public void E016Style_UnboundExtUrcb_IsNoDataSetFromPositiveSclEvidence() + public void SourceUnboundDynamicRcb_RemainsUnknownBeforeLiveCheck_NotOperationalFailure() { var availability = RcbExportEvidencePolicy.SourceAvailability( liveAvailability: null, @@ -28,7 +28,8 @@ public void E016Style_UnboundExtUrcb_IsNoDataSetFromPositiveSclEvidence() dataSetResolved: false, configuredMemberCount: 0); - Assert.Equal(MmsRcbOperationalAvailability.NoDataSet, availability); + Assert.Equal(MmsRcbOperationalAvailability.Unknown, availability); + Assert.NotEqual(MmsRcbOperationalAvailability.NoDataSet, availability); } [Fact] @@ -99,21 +100,40 @@ public void SourceConfiguredButLiveVerifiedNone_IsConfigurationConflict() } [Fact] - public void SourceUnboundButLiveVerifiedDataSet_IsConfigurationConflict() + public void SourceUnboundButLiveVerifiedDataSet_IsDynamicBinding_NotConfigurationConflict() { - Assert.True(RcbExportEvidencePolicy.HasSourceLiveBindingConflict( + const string live = "AA1C1F13R4Application/LLN0.AR_HYB_01"; + + Assert.True(RcbExportEvidencePolicy.IsDynamicRuntimeBinding( string.Empty, - "E016MD66CTRL/LLN0.DataSet", + live, + MmsRcbDataSetProbeState.ReadSucceeded)); + Assert.False(RcbExportEvidencePolicy.HasSourceLiveBindingConflict( + string.Empty, + live, + MmsRcbDataSetProbeState.ReadSucceeded)); + } + + [Fact] + public void SourceConfiguredAndLiveVerifiedDifferentDataSet_IsConfigurationConflict() + { + Assert.True(RcbExportEvidencePolicy.HasSourceLiveBindingConflict( + "AA1C1F13R4Application/LLN0.StaticSet", + "AA1C1F13R4Application/LLN0.AR_HYB_01", MmsRcbDataSetProbeState.ReadSucceeded)); } [Fact] - public void FailedLiveRead_DoesNotCreateFalseConfigurationConflict() + public void FailedLiveRead_DoesNotCreateFalseConfigurationConflictOrDynamicBinding() { Assert.False(RcbExportEvidencePolicy.HasSourceLiveBindingConflict( "E016MD66CTRL/LLN0.DataSet", string.Empty, MmsRcbDataSetProbeState.ReadFailed)); + Assert.False(RcbExportEvidencePolicy.IsDynamicRuntimeBinding( + string.Empty, + "E016MD66CTRL/LLN0.RuntimeSet", + MmsRcbDataSetProbeState.ReadFailed)); } [Fact] @@ -125,6 +145,19 @@ public void EquivalentSourceAndLiveBindings_AreNotConfigurationConflict() MmsRcbDataSetProbeState.ReadSucceeded)); } + [Fact] + public void DynamicSourceReason_ExplainsUnboundSourceAsValidBeforeLiveCheck() + { + var reason = RcbExportEvidencePolicy.SourceReason( + configuredDataSetName: string.Empty, + dataSetResolved: false, + configuredMemberCount: 0, + connected: true); + + Assert.Contains("valid for a dynamic RCB", reason, StringComparison.OrdinalIgnoreCase); + Assert.Contains("Check Availability", reason, StringComparison.OrdinalIgnoreCase); + } + [Fact] public void DuplicateShortRcbNames_AreDistinguishedByLogicalScope() { From 01d81fcf600eeee121aa96822f83625927b3dc4e Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 16 Aug 2026 19:46:28 +0700 Subject: [PATCH 3/4] Reserve internal safe area for edition toggles --- SaveSclWindow.xaml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/SaveSclWindow.xaml b/SaveSclWindow.xaml index cd68d368..cbd81238 100644 --- a/SaveSclWindow.xaml +++ b/SaveSclWindow.xaml @@ -14,6 +14,9 @@