From 362d2b9701b9222528bc98f5e9fcfeb394e3eb6e Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Tue, 6 Oct 2026 14:27:22 +0700 Subject: [PATCH 01/14] fix(scl): consume engine-owned association resolution --- Services/SclAssistedConnectionPreparation.cs | 54 +++++++++++++------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/Services/SclAssistedConnectionPreparation.cs b/Services/SclAssistedConnectionPreparation.cs index c328b884d..3646961c0 100644 --- a/Services/SclAssistedConnectionPreparation.cs +++ b/Services/SclAssistedConnectionPreparation.cs @@ -7,6 +7,7 @@ namespace ArIED61850Tester.Services; public sealed class SclAssistedConnectionPreparation { public ArScl.SclAssistedMmsAssociationPlan? AssociationPlan { get; init; } + public ArScl.SclAssistedMmsAssociationResolution? AssociationResolution { get; init; } public ArScl.SclMmsDomainInventory DomainInventory { get; init; } = new(); public ArScl.SclInitialFcReadDesign? InitialReadDesign { get; init; } public ArMms.InitialFcReadPlan? InitialReadPlan { get; init; } @@ -14,7 +15,7 @@ public sealed class SclAssistedConnectionPreparation public IReadOnlyList Warnings { get; init; } = Array.Empty(); public bool IsSuccess => Errors.Count == 0 && - AssociationPlan is not null && + AssociationResolution?.IsSuccess == true && DomainInventory.IsSuccess && InitialReadDesign?.IsSuccess == true && InitialReadPlan?.IsValid == true; @@ -126,28 +127,42 @@ public static SclAssistedConnectionPreparation Build( // association. The previous SclInteroperabilityDefault changed source TSEL // to 0000 and calling AE qualifier to 23, so a generated SCL could round-trip // its remote/called identity while still emitting a different wire handshake. - var association = ArScl.SclAssistedMmsAssociationPlanBuilder.BuildExact( + // Preserve the exact complete-SCL plan as a compatibility/round-trip contract, + // but do not treat an incomplete optional association identity as fatal. The + // engine resolver distinguishes missing from invalid/ambiguous fields and owns + // every interoperability candidate. + var exactAssociation = ArScl.SclAssistedMmsAssociationPlanBuilder.BuildExact( effectiveRemote, ArScl.MmsLocalAssociationProfile.ExistingRuntimeDefault); - warnings.AddRange(association.Warnings); - if (!association.IsSuccess || association.Plan is null) + warnings.AddRange(exactAssociation.Warnings); + + ArScl.SclAssistedMmsAssociationPlan? runtimePlan = null; + if (exactAssociation.IsSuccess && exactAssociation.Plan is not null) { - errors.AddRange(association.Errors); - return Fail(errors, warnings); + runtimePlan = new ArScl.SclAssistedMmsAssociationPlan + { + Host = normalizedHost, + Port = normalizedPort, + IedName = exactAssociation.Plan.IedName, + AccessPointName = exactAssociation.Plan.AccessPointName, + LocalProfileName = exactAssociation.Plan.LocalProfileName, + Cotp = exactAssociation.Plan.Cotp, + Association = exactAssociation.Plan.Association, + CotpConnectRequest = exactAssociation.Plan.CotpConnectRequest, + SessionPresentationAcseMmsRequest = exactAssociation.Plan.SessionPresentationAcseMmsRequest + }; } - var runtimePlan = new ArScl.SclAssistedMmsAssociationPlan + var associationResolution = ArScl.SclAssistedMmsAssociationCandidateResolver.Resolve( + effectiveRemote, + ArScl.MmsLocalAssociationProfile.ExistingRuntimeDefault, + normalizedPort); + warnings.AddRange(associationResolution.Warnings); + if (!associationResolution.IsSuccess) { - Host = normalizedHost, - Port = normalizedPort, - IedName = association.Plan.IedName, - AccessPointName = association.Plan.AccessPointName, - LocalProfileName = association.Plan.LocalProfileName, - Cotp = association.Plan.Cotp, - Association = association.Plan.Association, - CotpConnectRequest = association.Plan.CotpConnectRequest, - SessionPresentationAcseMmsRequest = association.Plan.SessionPresentationAcseMmsRequest - }; + errors.AddRange(associationResolution.Errors); + return Fail(errors, warnings, associationResolution); + } var domains = ArScl.SclMmsDomainInventoryReader.Read(sclXml, normalizedIed, normalizedAccessPoint); warnings.AddRange(domains.Warnings); @@ -174,6 +189,7 @@ public static SclAssistedConnectionPreparation Build( return new SclAssistedConnectionPreparation { AssociationPlan = runtimePlan, + AssociationResolution = associationResolution, DomainInventory = domains, InitialReadDesign = design, InitialReadPlan = initialReadPlan, @@ -184,9 +200,11 @@ public static SclAssistedConnectionPreparation Build( private static SclAssistedConnectionPreparation Fail( IReadOnlyCollection errors, - IReadOnlyCollection warnings) + IReadOnlyCollection warnings, + ArScl.SclAssistedMmsAssociationResolution? associationResolution = null) => new() { + AssociationResolution = associationResolution, Errors = errors.Where(message => !string.IsNullOrWhiteSpace(message)).Distinct(StringComparer.Ordinal).ToArray(), Warnings = warnings.Where(message => !string.IsNullOrWhiteSpace(message)).Distinct(StringComparer.Ordinal).ToArray() }; From 4054b283008c6f2535503a746ef58c245d33bd55 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Tue, 6 Oct 2026 14:27:35 +0700 Subject: [PATCH 02/14] fix(scl): use bounded smart association candidates for Open SCL --- Services/NativeIec61850Client.SclAssisted.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Services/NativeIec61850Client.SclAssisted.cs b/Services/NativeIec61850Client.SclAssisted.cs index b54a4d373..6dde40500 100644 --- a/Services/NativeIec61850Client.SclAssisted.cs +++ b/Services/NativeIec61850Client.SclAssisted.cs @@ -138,7 +138,7 @@ public async Task ConnectUsingSclAsync( _port, maximumVariableReferencesPerRead); if (!preparation.IsSuccess || - preparation.AssociationPlan is null || + preparation.AssociationResolution is null || preparation.InitialReadDesign is null || preparation.InitialReadPlan is null) { @@ -161,7 +161,7 @@ preparation.InitialReadDesign is null || { var associationWatch = Stopwatch.StartNew(); var online = await _session.ConnectSclAssistedAsync( - preparation.AssociationPlan, + preparation.AssociationResolution, preparation.DomainInventory, TimeSpan.FromSeconds(8), cancellationToken).ConfigureAwait(false); @@ -324,7 +324,9 @@ or ArMms.InitialFcReadExecutionStatus.TimedOut var partial = initialRead.Status == ArMms.InitialFcReadExecutionStatus.Partial || initialValueCacheLoss > 0; LastDiscoverySummary = - $"SCL-assisted MMS: domains={reconciledDomains.Count}, extraOnlineDomains={extraDomains}, " + + $"SCL-assisted MMS: associationCandidate={online.SelectedAssociationCandidateName}, " + + $"associationSource={online.SelectedAssociationCandidateSource}, associationAttempts={online.AssociationAttemptCount}, " + + $"domains={reconciledDomains.Count}, extraOnlineDomains={extraDomains}, " + $"initialTargets={initialRead.Plan.Targets.Count}, fcRootTargets={fcRootTargets}, doScopedTargets={dataObjectScopedTargets}, " + $"successfulReads={initialRead.SuccessfulTargetCount}, failedReads={initialRead.FailedTargetCount}, projectedLeaves={initialRead.ProjectedLeafCount}, " + $"projectedUniqueValues={projectedUniqueValues}, initialValueCache={_trustedSclInitialValues.Count}, cacheLoss={initialValueCacheLoss}, " + @@ -342,6 +344,7 @@ or ArMms.InitialFcReadExecutionStatus.TimedOut : string.Empty; var warnings = preparation.Warnings + .Concat(online.AssociationResolutionNotes) .Concat(extraDomains > 0 ? new[] { $"IED exposes {extraDomains} extra online MMS domain(s); they remain evidence only and do not mutate the SCL model." } : Array.Empty()) From a282625594df4537e08b5daecf996c6aab9eb3e1 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Tue, 6 Oct 2026 14:27:56 +0700 Subject: [PATCH 03/14] test(scl): lock smart association consumer boundary --- ...veryStaticWorkflowParityRegressionTests.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/ARSAS.Tests/DiscoveryStaticWorkflowParityRegressionTests.cs b/tests/ARSAS.Tests/DiscoveryStaticWorkflowParityRegressionTests.cs index fd103eafe..dcbe69b00 100644 --- a/tests/ARSAS.Tests/DiscoveryStaticWorkflowParityRegressionTests.cs +++ b/tests/ARSAS.Tests/DiscoveryStaticWorkflowParityRegressionTests.cs @@ -213,6 +213,37 @@ public void ExistingOpenedModelWorkflow_RemainsTaskFirstAndStaticReportOnly() Assert.Contains("cyclic MMS process polling and dynamic DataSet writes remain disabled", shared, StringComparison.Ordinal); } + [Fact] + public void OpenScl_Uses_EngineOwned_Bounded_Association_Resolution_Without_Discovery_Fallback() + { + var preparation = Read("Services/SclAssistedConnectionPreparation.cs"); + Assert.Contains( + "SclAssistedMmsAssociationCandidateResolver.Resolve", + preparation, + StringComparison.Ordinal); + Assert.Contains("AssociationResolution = associationResolution", preparation, StringComparison.Ordinal); + Assert.DoesNotContain("errors.AddRange(exactAssociation.Errors)", preparation, StringComparison.Ordinal); + + var client = Read("Services/NativeIec61850Client.SclAssisted.cs"); + var start = client.IndexOf( + "public async Task ConnectUsingSclAsync", + StringComparison.Ordinal); + var end = client.IndexOf( + "private static ArMms.MmsReportInventory BuildTrustedSclReportInventory", + start, + StringComparison.Ordinal); + + Assert.True(start >= 0 && end > start); + var connectFlow = client[start..end]; + + Assert.Contains("preparation.AssociationResolution", connectFlow, StringComparison.Ordinal); + Assert.Contains("_session.ConnectSclAssistedAsync", connectFlow, StringComparison.Ordinal); + Assert.Contains("online.SelectedAssociationCandidateName", connectFlow, StringComparison.Ordinal); + Assert.Contains("online.AssociationAttemptCount", connectFlow, StringComparison.Ordinal); + Assert.DoesNotContain(".DiscoverAsync(", connectFlow, StringComparison.Ordinal); + Assert.DoesNotContain("DiscoverSignals", connectFlow, StringComparison.Ordinal); + } + private static string Read(string relativePath) => File.ReadAllText(FindRepoFile(relativePath)).Replace("\r\n", "\n", StringComparison.Ordinal); From 9c6ad06f5ce382f863bb6d0111f3cb4f55aa42af Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Tue, 6 Oct 2026 14:36:37 +0700 Subject: [PATCH 04/14] test(scl): reproduce incomplete CID association preparation safely --- ...nectionPreparationInteroperabilityTests.cs | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 tests/ARSAS.Tests/SclAssistedConnectionPreparationInteroperabilityTests.cs diff --git a/tests/ARSAS.Tests/SclAssistedConnectionPreparationInteroperabilityTests.cs b/tests/ARSAS.Tests/SclAssistedConnectionPreparationInteroperabilityTests.cs new file mode 100644 index 000000000..f1d115d02 --- /dev/null +++ b/tests/ARSAS.Tests/SclAssistedConnectionPreparationInteroperabilityTests.cs @@ -0,0 +1,92 @@ +using AR.Iec61850.Scl; +using ArIED61850Tester.Services; + +namespace ARSAS.Tests; + +public sealed class SclAssistedConnectionPreparationInteroperabilityTests +{ + [Fact] + public void Incomplete_Cid_Association_Identity_Remains_Connectable_Without_Discovery_Fallback() + { + const string xml = """ + + + + +
+

192.0.2.10

+

255.255.255.0

+

00000001

+

0001

+

0001

+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ """; + + var preparation = SclAssistedConnectionPreparationBuilder.Build( + xml, + "IED1", + "P1", + "192.0.2.10", + 102); + + Assert.True(preparation.IsSuccess, string.Join(" | ", preparation.Errors)); + Assert.Null(preparation.AssociationPlan); + + var resolution = Assert.IsType( + preparation.AssociationResolution); + Assert.True(resolution.IsSuccess, string.Join(" | ", resolution.Errors)); + Assert.InRange( + resolution.Candidates.Count, + 1, + SclAssistedMmsAssociationCandidateResolver.MaximumCandidateCount); + Assert.Contains( + resolution.Fields, + field => + field.Name == "OSI-AP-Title" && + field.State == SclAssociationFieldState.Unspecified); + Assert.Contains( + resolution.Fields, + field => + field.Name == "OSI-AE-Qualifier" && + field.State == SclAssociationFieldState.Unspecified); + + Assert.Equal(new[] { "EXACT_APP_DOMAIN" }, preparation.DomainInventory.ExpectedDomains); + Assert.Single(preparation.InitialReadDesign!.Model.DataSets); + Assert.Single(preparation.InitialReadDesign.Model.ReportControls); + Assert.NotNull(preparation.InitialReadPlan); + + // This fixture proves preparation/model convergence only. No network or full + // discovery API is invoked from the pure preparation builder. + } +} From d2438887f45c7f18ef1f59e9d5a4539488fc8e53 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Tue, 6 Oct 2026 14:37:49 +0700 Subject: [PATCH 05/14] chore(engine): pin smart SCL association candidate --- engines/ARIEC61850.lock.json | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/engines/ARIEC61850.lock.json b/engines/ARIEC61850.lock.json index 360e72a96..247512d27 100644 --- a/engines/ARIEC61850.lock.json +++ b/engines/ARIEC61850.lock.json @@ -1,10 +1,10 @@ { "schemaVersion": 1, "repository": "masarray/ARIEC61850", - "ref": "main", - "commit": "648124097621046f5f127ceb1cf853fea54db730", - "sourcePullRequest": 135, - "purpose": "R7 physical SCL repair pin on the complete PR #134 smart-discovery performance head. Accepted remote AP-title/AE/PSEL/SSEL remain decoded from the exact association request bytes accepted by the IED and TSEL remains bound to the accepted COTP destination selector. Physical AA1E1F06R4 evidence additionally proved that indexed RCB siblings may differ in writable/current BufTm, IntgPd, TrgOps and OptFlds while still representing one logical SCL ReportControl; engine head 648124097621046f5f127ceb1cf853fea54db730 preserves DataSet/ConfRev/domain/LN/buffered identity while allowing that runtime-mutable setting drift. ARSAS consumer-side reconnect must reproduce the proven native calling identity byte-for-byte and canonical save uses full-model SCL with prefixed-LN identity, CDC-aware WYE/DEL/SEQ SDO and FC ownership, plus exact standard TCTR/TVTR/LTIM/EEName/MltLev CDC authority and Edition-2-only LTRK service-tracking CDCs with Edition-1 schema downgrade protection; physical R8 reuse evidence additionally requires exact MMS TypeSpecification declaration order through canonical model/SCL reload, treats SG/SE as setting data rather than control blocks, and maps MHAI THD phase groups as WYE/CMV; safety is enforced by bounded FC-read policy rather than deleting discovered model leaves. Production promotion remains fail-closed and still requires fresh physical authority. R9 reuse lock additionally preserves rptID-backed preallocated singleton RCB indexing, case-distinct canonical instance values, and compile-safe logical RCB projection; unresolved 46 trusted-SCL projection errors remain a fail-open diagnostic gap but a fail-closed promotion gap until physical evidence reaches zero. P1 trusted-SCL projection repair removes cross-DO positional dependence for multi-DO CF structures: SCL LNodeType order is not treated as MMS FC-structure order, so CF hydration is split into exact DO-scoped structured Reads and remains batched/bounded. P0 structural discovery remains unchanged. P2 makes instance-value identity exact-case end-to-end: ARSAS trusted-SCL caching uses StringComparer.Ordinal and reports projectedUniqueValues/cacheLoss; engine TypeSpecification member resolution and canonical DO/DA instance-value targeting are case-sensitive so legal paths such as tracking t/T cannot collapse or cross-resolve. R10 physical acceptance passed on AA1E1F06R4: Ed2 and Ed1 both reached projectionErrors=0 and cacheLoss=0, all planned reads succeeded, 58/58 runtime points were report-backed, and actual InformationReport traffic was observed. The exact tested commit 9935d6902d786cc69b299260fe36b835944d5e81 and merged main commit 648124097621046f5f127ceb1cf853fea54db730 have the identical source tree 1cf7e08f333f24994625e8fe8416dbd0a16195b1.", + "ref": "fix/scl-smart-association-142", + "commit": "84e9820e5a32690475960e49d5ef74e6637847fd", + "sourcePullRequest": 143, + "purpose": "Post-v1.6.40 integration candidate for engine-owned SCL-assisted MMS association resolution. Missing ConnectedAP association fields are unresolved rather than fabricated: explicit valid SCL fields remain immutable constraints, malformed/conflicting declarations fail closed, candidates are bounded and serial with fresh transport, and successful SCL-assisted association still performs Domain/VMD validation only without full discovery fallback. This candidate is not a replacement for the physically accepted v1.6.40 baseline; physicalTestedCommit and mergedMainCommit below remain the field authority until a new physical acceptance is recorded. R7 physical SCL repair pin on the complete PR #134 smart-discovery performance head. Accepted remote AP-title/AE/PSEL/SSEL remain decoded from the exact association request bytes accepted by the IED and TSEL remains bound to the accepted COTP destination selector. Physical AA1E1F06R4 evidence additionally proved that indexed RCB siblings may differ in writable/current BufTm, IntgPd, TrgOps and OptFlds while still representing one logical SCL ReportControl; engine head 648124097621046f5f127ceb1cf853fea54db730 preserves DataSet/ConfRev/domain/LN/buffered identity while allowing that runtime-mutable setting drift. ARSAS consumer-side reconnect must reproduce the proven native calling identity byte-for-byte and canonical save uses full-model SCL with prefixed-LN identity, CDC-aware WYE/DEL/SEQ SDO and FC ownership, plus exact standard TCTR/TVTR/LTIM/EEName/MltLev CDC authority and Edition-2-only LTRK service-tracking CDCs with Edition-1 schema downgrade protection; physical R8 reuse evidence additionally requires exact MMS TypeSpecification declaration order through canonical model/SCL reload, treats SG/SE as setting data rather than control blocks, and maps MHAI THD phase groups as WYE/CMV; safety is enforced by bounded FC-read policy rather than deleting discovered model leaves. Production promotion remains fail-closed and still requires fresh physical authority. R9 reuse lock additionally preserves rptID-backed preallocated singleton RCB indexing, case-distinct canonical instance values, and compile-safe logical RCB projection; unresolved 46 trusted-SCL projection errors remain a fail-open diagnostic gap but a fail-closed promotion gap until physical evidence reaches zero. P1 trusted-SCL projection repair removes cross-DO positional dependence for multi-DO CF structures: SCL LNodeType order is not treated as MMS FC-structure order, so CF hydration is split into exact DO-scoped structured Reads and remains batched/bounded. P0 structural discovery remains unchanged. P2 makes instance-value identity exact-case end-to-end: ARSAS trusted-SCL caching uses StringComparer.Ordinal and reports projectedUniqueValues/cacheLoss; engine TypeSpecification member resolution and canonical DO/DA instance-value targeting are case-sensitive so legal paths such as tracking t/T cannot collapse or cross-resolve. R10 physical acceptance passed on AA1E1F06R4: Ed2 and Ed1 both reached projectionErrors=0 and cacheLoss=0, all planned reads succeeded, 58/58 runtime points were report-backed, and actual InformationReport traffic was observed. The exact tested commit 9935d6902d786cc69b299260fe36b835944d5e81 and merged main commit 648124097621046f5f127ceb1cf853fea54db730 have the identical source tree 1cf7e08f333f24994625e8fe8416dbd0a16195b1.", "previousTrialPin": { "commit": "4467124775d8d9d76f3db194f9fbfd97144767a8", "sourcePullRequest": 134, @@ -22,5 +22,10 @@ }, "physicalTestedCommit": "9935d6902d786cc69b299260fe36b835944d5e81", "mergedMainCommit": "648124097621046f5f127ceb1cf853fea54db730", - "mergedMainTree": "1cf7e08f333f24994625e8fe8416dbd0a16195b1" + "mergedMainTree": "1cf7e08f333f24994625e8fe8416dbd0a16195b1", + "previousStablePin": { + "commit": "648124097621046f5f127ceb1cf853fea54db730", + "sourcePullRequest": 135, + "purpose": "ARSAS v1.6.40 merged/physically accepted SCL + Smart Discovery baseline retained unchanged while the SCL association interoperability candidate is evaluated." + } } From 4790c64c9782084cf67bc6ea4155a9395470a8d2 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Tue, 6 Oct 2026 14:37:53 +0700 Subject: [PATCH 06/14] test(engine): preserve physical baseline across candidate pin --- tests/ARSAS.Tests/SclExportSemanticParityPatchTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ARSAS.Tests/SclExportSemanticParityPatchTests.cs b/tests/ARSAS.Tests/SclExportSemanticParityPatchTests.cs index 35f55d202..ff22c61ba 100644 --- a/tests/ARSAS.Tests/SclExportSemanticParityPatchTests.cs +++ b/tests/ARSAS.Tests/SclExportSemanticParityPatchTests.cs @@ -170,11 +170,11 @@ public void SaveHook_IsAfterCanonicalSerialization_AndBeforeReloadValidation() var engineLock = ReadRepoFile("engines/ARIEC61850.lock.json"); Assert.Contains( - "\"commit\": \"648124097621046f5f127ceb1cf853fea54db730\"", + "\"mergedMainCommit\": \"648124097621046f5f127ceb1cf853fea54db730\"", engineLock, StringComparison.Ordinal); Assert.Contains( - "\"sourcePullRequest\": 135", + "\"physicalTestedCommit\": \"9935d6902d786cc69b299260fe36b835944d5e81\"", engineLock, StringComparison.Ordinal); } From eafecaa42d827875f98fdf8fd8e2c1dd716a63e0 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Tue, 6 Oct 2026 14:37:56 +0700 Subject: [PATCH 07/14] test(engine): distinguish candidate pin from physical authority --- tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs b/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs index 745cf9c02..4905506d9 100644 --- a/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs +++ b/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs @@ -401,18 +401,20 @@ public void SourceClean_GuardsEveryTrackedFileWithoutWholeFileExceptions() [Fact] - public void EnginePin_MatchesPhysicalSclRepairHead() + public void EngineLock_PreservesPhysicalSclRepairBaselineAcrossCandidatePins() { var lockFile = File.ReadAllText(FindRepoFile("engines/ARIEC61850.lock.json")); Assert.Contains( - "\"commit\": \"648124097621046f5f127ceb1cf853fea54db730\"", + "\"mergedMainCommit\": \"648124097621046f5f127ceb1cf853fea54db730\"", lockFile, StringComparison.Ordinal); Assert.Contains( "\"physicalTestedCommit\": \"9935d6902d786cc69b299260fe36b835944d5e81\"", lockFile, StringComparison.Ordinal); + Assert.Contains("\"sourcePullRequest\": 143", lockFile, StringComparison.Ordinal); + Assert.Contains("\"previousStablePin\"", lockFile, StringComparison.Ordinal); Assert.Contains("\"sourcePullRequest\": 135", lockFile, StringComparison.Ordinal); Assert.Contains("exact association request bytes accepted by the IED", lockFile, StringComparison.Ordinal); Assert.Contains("accepted COTP destination selector", lockFile, StringComparison.Ordinal); From fbb3abce2fffd63b934c019d757e950b5b721d9f Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Tue, 6 Oct 2026 14:40:03 +0700 Subject: [PATCH 08/14] fix(engine): keep canonical lock ref while pinning candidate SHA --- engines/ARIEC61850.lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/engines/ARIEC61850.lock.json b/engines/ARIEC61850.lock.json index 247512d27..bcd2705b2 100644 --- a/engines/ARIEC61850.lock.json +++ b/engines/ARIEC61850.lock.json @@ -1,10 +1,10 @@ { "schemaVersion": 1, "repository": "masarray/ARIEC61850", - "ref": "fix/scl-smart-association-142", + "ref": "main", "commit": "84e9820e5a32690475960e49d5ef74e6637847fd", "sourcePullRequest": 143, - "purpose": "Post-v1.6.40 integration candidate for engine-owned SCL-assisted MMS association resolution. Missing ConnectedAP association fields are unresolved rather than fabricated: explicit valid SCL fields remain immutable constraints, malformed/conflicting declarations fail closed, candidates are bounded and serial with fresh transport, and successful SCL-assisted association still performs Domain/VMD validation only without full discovery fallback. This candidate is not a replacement for the physically accepted v1.6.40 baseline; physicalTestedCommit and mergedMainCommit below remain the field authority until a new physical acceptance is recorded. R7 physical SCL repair pin on the complete PR #134 smart-discovery performance head. Accepted remote AP-title/AE/PSEL/SSEL remain decoded from the exact association request bytes accepted by the IED and TSEL remains bound to the accepted COTP destination selector. Physical AA1E1F06R4 evidence additionally proved that indexed RCB siblings may differ in writable/current BufTm, IntgPd, TrgOps and OptFlds while still representing one logical SCL ReportControl; engine head 648124097621046f5f127ceb1cf853fea54db730 preserves DataSet/ConfRev/domain/LN/buffered identity while allowing that runtime-mutable setting drift. ARSAS consumer-side reconnect must reproduce the proven native calling identity byte-for-byte and canonical save uses full-model SCL with prefixed-LN identity, CDC-aware WYE/DEL/SEQ SDO and FC ownership, plus exact standard TCTR/TVTR/LTIM/EEName/MltLev CDC authority and Edition-2-only LTRK service-tracking CDCs with Edition-1 schema downgrade protection; physical R8 reuse evidence additionally requires exact MMS TypeSpecification declaration order through canonical model/SCL reload, treats SG/SE as setting data rather than control blocks, and maps MHAI THD phase groups as WYE/CMV; safety is enforced by bounded FC-read policy rather than deleting discovered model leaves. Production promotion remains fail-closed and still requires fresh physical authority. R9 reuse lock additionally preserves rptID-backed preallocated singleton RCB indexing, case-distinct canonical instance values, and compile-safe logical RCB projection; unresolved 46 trusted-SCL projection errors remain a fail-open diagnostic gap but a fail-closed promotion gap until physical evidence reaches zero. P1 trusted-SCL projection repair removes cross-DO positional dependence for multi-DO CF structures: SCL LNodeType order is not treated as MMS FC-structure order, so CF hydration is split into exact DO-scoped structured Reads and remains batched/bounded. P0 structural discovery remains unchanged. P2 makes instance-value identity exact-case end-to-end: ARSAS trusted-SCL caching uses StringComparer.Ordinal and reports projectedUniqueValues/cacheLoss; engine TypeSpecification member resolution and canonical DO/DA instance-value targeting are case-sensitive so legal paths such as tracking t/T cannot collapse or cross-resolve. R10 physical acceptance passed on AA1E1F06R4: Ed2 and Ed1 both reached projectionErrors=0 and cacheLoss=0, all planned reads succeeded, 58/58 runtime points were report-backed, and actual InformationReport traffic was observed. The exact tested commit 9935d6902d786cc69b299260fe36b835944d5e81 and merged main commit 648124097621046f5f127ceb1cf853fea54db730 have the identical source tree 1cf7e08f333f24994625e8fe8416dbd0a16195b1.", + "purpose": "Post-v1.6.40 exact-SHA integration candidate for engine-owned SCL-assisted MMS association resolution. Missing ConnectedAP association fields are unresolved rather than fabricated: explicit valid SCL fields remain immutable constraints, malformed/conflicting declarations fail closed, candidates are bounded and serial with fresh transport, and successful SCL-assisted association still performs Domain/VMD validation only without full discovery fallback. This candidate is not a replacement for the physically accepted v1.6.40 baseline; physicalTestedCommit and mergedMainCommit below remain the field authority until a new physical acceptance is recorded. R7 physical SCL repair pin on the complete PR #134 smart-discovery performance head. Accepted remote AP-title/AE/PSEL/SSEL remain decoded from the exact association request bytes accepted by the IED and TSEL remains bound to the accepted COTP destination selector. Physical AA1E1F06R4 evidence additionally proved that indexed RCB siblings may differ in writable/current BufTm, IntgPd, TrgOps and OptFlds while still representing one logical SCL ReportControl; engine head 648124097621046f5f127ceb1cf853fea54db730 preserves DataSet/ConfRev/domain/LN/buffered identity while allowing that runtime-mutable setting drift. ARSAS consumer-side reconnect must reproduce the proven native calling identity byte-for-byte and canonical save uses full-model SCL with prefixed-LN identity, CDC-aware WYE/DEL/SEQ SDO and FC ownership, plus exact standard TCTR/TVTR/LTIM/EEName/MltLev CDC authority and Edition-2-only LTRK service-tracking CDCs with Edition-1 schema downgrade protection; physical R8 reuse evidence additionally requires exact MMS TypeSpecification declaration order through canonical model/SCL reload, treats SG/SE as setting data rather than control blocks, and maps MHAI THD phase groups as WYE/CMV; safety is enforced by bounded FC-read policy rather than deleting discovered model leaves. Production promotion remains fail-closed and still requires fresh physical authority. R9 reuse lock additionally preserves rptID-backed preallocated singleton RCB indexing, case-distinct canonical instance values, and compile-safe logical RCB projection; unresolved 46 trusted-SCL projection errors remain a fail-open diagnostic gap but a fail-closed promotion gap until physical evidence reaches zero. P1 trusted-SCL projection repair removes cross-DO positional dependence for multi-DO CF structures: SCL LNodeType order is not treated as MMS FC-structure order, so CF hydration is split into exact DO-scoped structured Reads and remains batched/bounded. P0 structural discovery remains unchanged. P2 makes instance-value identity exact-case end-to-end: ARSAS trusted-SCL caching uses StringComparer.Ordinal and reports projectedUniqueValues/cacheLoss; engine TypeSpecification member resolution and canonical DO/DA instance-value targeting are case-sensitive so legal paths such as tracking t/T cannot collapse or cross-resolve. R10 physical acceptance passed on AA1E1F06R4: Ed2 and Ed1 both reached projectionErrors=0 and cacheLoss=0, all planned reads succeeded, 58/58 runtime points were report-backed, and actual InformationReport traffic was observed. The exact tested commit 9935d6902d786cc69b299260fe36b835944d5e81 and merged main commit 648124097621046f5f127ceb1cf853fea54db730 have the identical source tree 1cf7e08f333f24994625e8fe8416dbd0a16195b1.", "previousTrialPin": { "commit": "4467124775d8d9d76f3db194f9fbfd97144767a8", "sourcePullRequest": 134, From c0b346c61a2bd579af72a6ccc0cc2c6483b7285f Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Tue, 6 Oct 2026 21:06:16 +0700 Subject: [PATCH 09/14] evidence(scl): record physically accepted association layer --- engines/ARIEC61850.lock.json | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/engines/ARIEC61850.lock.json b/engines/ARIEC61850.lock.json index bcd2705b2..7c9c8aa31 100644 --- a/engines/ARIEC61850.lock.json +++ b/engines/ARIEC61850.lock.json @@ -4,7 +4,7 @@ "ref": "main", "commit": "84e9820e5a32690475960e49d5ef74e6637847fd", "sourcePullRequest": 143, - "purpose": "Post-v1.6.40 exact-SHA integration candidate for engine-owned SCL-assisted MMS association resolution. Missing ConnectedAP association fields are unresolved rather than fabricated: explicit valid SCL fields remain immutable constraints, malformed/conflicting declarations fail closed, candidates are bounded and serial with fresh transport, and successful SCL-assisted association still performs Domain/VMD validation only without full discovery fallback. This candidate is not a replacement for the physically accepted v1.6.40 baseline; physicalTestedCommit and mergedMainCommit below remain the field authority until a new physical acceptance is recorded. R7 physical SCL repair pin on the complete PR #134 smart-discovery performance head. Accepted remote AP-title/AE/PSEL/SSEL remain decoded from the exact association request bytes accepted by the IED and TSEL remains bound to the accepted COTP destination selector. Physical AA1E1F06R4 evidence additionally proved that indexed RCB siblings may differ in writable/current BufTm, IntgPd, TrgOps and OptFlds while still representing one logical SCL ReportControl; engine head 648124097621046f5f127ceb1cf853fea54db730 preserves DataSet/ConfRev/domain/LN/buffered identity while allowing that runtime-mutable setting drift. ARSAS consumer-side reconnect must reproduce the proven native calling identity byte-for-byte and canonical save uses full-model SCL with prefixed-LN identity, CDC-aware WYE/DEL/SEQ SDO and FC ownership, plus exact standard TCTR/TVTR/LTIM/EEName/MltLev CDC authority and Edition-2-only LTRK service-tracking CDCs with Edition-1 schema downgrade protection; physical R8 reuse evidence additionally requires exact MMS TypeSpecification declaration order through canonical model/SCL reload, treats SG/SE as setting data rather than control blocks, and maps MHAI THD phase groups as WYE/CMV; safety is enforced by bounded FC-read policy rather than deleting discovered model leaves. Production promotion remains fail-closed and still requires fresh physical authority. R9 reuse lock additionally preserves rptID-backed preallocated singleton RCB indexing, case-distinct canonical instance values, and compile-safe logical RCB projection; unresolved 46 trusted-SCL projection errors remain a fail-open diagnostic gap but a fail-closed promotion gap until physical evidence reaches zero. P1 trusted-SCL projection repair removes cross-DO positional dependence for multi-DO CF structures: SCL LNodeType order is not treated as MMS FC-structure order, so CF hydration is split into exact DO-scoped structured Reads and remains batched/bounded. P0 structural discovery remains unchanged. P2 makes instance-value identity exact-case end-to-end: ARSAS trusted-SCL caching uses StringComparer.Ordinal and reports projectedUniqueValues/cacheLoss; engine TypeSpecification member resolution and canonical DO/DA instance-value targeting are case-sensitive so legal paths such as tracking t/T cannot collapse or cross-resolve. R10 physical acceptance passed on AA1E1F06R4: Ed2 and Ed1 both reached projectionErrors=0 and cacheLoss=0, all planned reads succeeded, 58/58 runtime points were report-backed, and actual InformationReport traffic was observed. The exact tested commit 9935d6902d786cc69b299260fe36b835944d5e81 and merged main commit 648124097621046f5f127ceb1cf853fea54db730 have the identical source tree 1cf7e08f333f24994625e8fe8416dbd0a16195b1.", + "purpose": "Physically accepted post-v1.6.40 SCL-assisted MMS association interoperability layer. Exact tested engine head 84e9820e5a32690475960e49d5ef74e6637847fd is preserved as the runtime pin; engine PR #143 is merged on main at e5deed1d8aa11d97991695c6e390baafea7ab797 with identical tree d92676dd6d6243ec997749dd0355aa8c36ad83bd. Missing ConnectedAP association fields remain unresolved rather than fabricated; explicit valid SCL fields are immutable constraints, malformed/conflicting declarations fail closed, candidates are bounded/serial with fresh transport, and successful SCL-assisted association performs Domain/VMD validation only with no full discovery fallback. The R10 discovery/model baseline remains independently frozen by physicalTestedCommit/mergedMainCommit/mergedMainTree and previousStablePin.", "previousTrialPin": { "commit": "4467124775d8d9d76f3db194f9fbfd97144767a8", "sourcePullRequest": 134, @@ -26,6 +26,19 @@ "previousStablePin": { "commit": "648124097621046f5f127ceb1cf853fea54db730", "sourcePullRequest": 135, - "purpose": "ARSAS v1.6.40 merged/physically accepted SCL + Smart Discovery baseline retained unchanged while the SCL association interoperability candidate is evaluated." + "purpose": "ARSAS v1.6.40 merged/physically accepted Smart Discovery + canonical SCL/R10 baseline retained unchanged as historical discovery/model authority beneath the separately accepted SCL-association interoperability layer." + }, + "sclAssociationInteroperability": { + "contractId": "SCL-ASSOC-PARTIAL-ADDR-PHYSICAL", + "status": "physically-accepted-engine-merged-consumer-pending-merge", + "sourcePullRequest": 143, + "testedEngineCommit": "84e9820e5a32690475960e49d5ef74e6637847fd", + "mergedEngineCommit": "e5deed1d8aa11d97991695c6e390baafea7ab797", + "engineTree": "d92676dd6d6243ec997749dd0355aa8c36ad83bd", + "consumerPullRequest": 425, + "consumerPhysicalMergeTestCommit": "9dc35d722b54872ed8c506eba062a80945761aa9", + "preservesR10DiscoveryModelBaseline": true, + "fullDiscoveryFallbackForbidden": true, + "cyclicMmsProcessPollingForbidden": true } } From 790823d4a5648cde3d6c957335243945005e7f7b Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Tue, 6 Oct 2026 21:06:20 +0700 Subject: [PATCH 10/14] evidence(scl): add post-baseline physical association acceptance --- .../interoperability-reference-target.json | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/evidence/interoperability-reference-target.json b/evidence/interoperability-reference-target.json index 5c4f00426..2fbf5c650 100644 --- a/evidence/interoperability-reference-target.json +++ b/evidence/interoperability-reference-target.json @@ -496,5 +496,57 @@ }, "actualInformationReportObserved": true } + }, + "postBaselineAcceptance": { + "sclAssociationInteroperability": { + "contractId": "SCL-ASSOC-PARTIAL-ADDR-PHYSICAL", + "status": "physically-accepted-engine-merged-consumer-pending-merge", + "scope": "Trusted-SCL reconnect when ConnectedAP omits called AP-title and/or AE qualifier while explicit IP/selectors remain authoritative.", + "preservesR10Baseline": true, + "engine": { + "pullRequest": 143, + "testedHead": "84e9820e5a32690475960e49d5ef74e6637847fd", + "mergedMain": "e5deed1d8aa11d97991695c6e390baafea7ab797", + "tree": "d92676dd6d6243ec997749dd0355aa8c36ad83bd" + }, + "consumer": { + "pullRequest": 425, + "testedMergeTreeHead": "9dc35d722b54872ed8c506eba062a80945761aa9" + }, + "physicalAcceptance": { + "date": "2026-10-06", + "associationProfile": "BalancedApTitle", + "associationAttempts": 1, + "expectedDomains": 5, + "observedDomains": 5, + "matchedDomains": 5, + "missingDomains": 0, + "extraDomains": 0, + "initialTargets": 395, + "successfulReads": 395, + "failedReads": 0, + "projectedLeaves": 4938, + "projectedUniqueValues": 4938, + "initialValueCache": 4938, + "cacheLoss": 0, + "projectionErrors": 0, + "fullDiscoverySkipped": true, + "staticDataSets": 1, + "staticMembers": 31, + "reportBackedRuntimePoints": 29, + "unresolvedRuntimePoints": 0, + "actualInformationReportObserved": true, + "cyclicMmsProcessPolling": 0 + }, + "invariants": { + "explicitSclAssociationFieldsRemainConstraints": true, + "malformedOrConflictingExplicitFieldsFailClosed": true, + "candidateEnumerationBounded": true, + "freshTransportPerCandidate": true, + "sourceSclNotMutated": true, + "noNamedVariableOrDatasetRediscovery": true, + "noFullDiscoveryFallback": true + } + } } } From 29584ff16b2ec3f39eb7dd0eb98248d5074b14c7 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Tue, 6 Oct 2026 21:06:24 +0700 Subject: [PATCH 11/14] docs(scl): record accepted post-baseline association contract --- docs/INTEROPERABILITY_REFERENCE_CONTRACT.md | 33 +++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/docs/INTEROPERABILITY_REFERENCE_CONTRACT.md b/docs/INTEROPERABILITY_REFERENCE_CONTRACT.md index 9d0b25d8f..f2dfbcd64 100644 --- a/docs/INTEROPERABILITY_REFERENCE_CONTRACT.md +++ b/docs/INTEROPERABILITY_REFERENCE_CONTRACT.md @@ -31,6 +31,39 @@ Merged engine provenance: - PR #135 → main merge `648124097621046f5f127ceb1cf853fea54db730`: canonical model, SCL interoperability, P1/P2 value pipeline. - ARSAS PR #324: consumer integration and physical R10 proof. +## Post-baseline SCL association interoperability — physically accepted + +A later, narrowly scoped interoperability layer extends trusted-SCL reconnect when a +ConnectedAP omits called AP-title and/or AE qualifier. It does **not** replace the +R10 Smart Discovery/model baseline above. + +Accepted source authority: + +- ARIEC61850 PR #143 tested head `84e9820e5a32690475960e49d5ef74e6637847fd`; +- engine main merge `e5deed1d8aa11d97991695c6e390baafea7ab797`; +- both engine commits have identical tree + `d92676dd6d6243ec997749dd0355aa8c36ad83bd`; +- ARSAS PR #425 physical merge-test tree + `9dc35d722b54872ed8c506eba062a80945761aa9`. + +The physical trusted-SCL session accepted the bounded `BalancedApTitle` candidate on +attempt 1, matched all 5 expected MMS domains, completed 395/395 bounded initial +reads, produced `projectionErrors=0` and `cacheLoss=0`, skipped full discovery, +kept cyclic MMS process polling at zero, and observed live InformationReport traffic +with final runtime unresolved count zero. + +The contract remains fail-closed: + +- explicit valid SCL association fields are immutable constraints; +- malformed or conflicting explicit values are not repaired by guessing; +- only genuinely unspecified fields may be completed by bounded engine-owned profiles; +- each candidate starts on a fresh transport; +- the source SCL is never rewritten by association negotiation; +- SCL-assisted success does not trigger NamedVariable/DataSet rediscovery or full discovery. + +The historical R10 baseline remains the discovery/model authority. This section is a +post-baseline accepted layer, not a rewrite of R10 evidence. + ## P0 — structural discovery freeze Contract: `P0-R9-STRUCTURAL`. From 44f330f766767718a6135695ade2573ba07b8405 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Tue, 6 Oct 2026 21:07:01 +0700 Subject: [PATCH 12/14] ci(scl): preserve R10 baseline under accepted association layer --- .github/workflows/scl-interoperability-r7.yml | 261 +++++++++++++++++- 1 file changed, 252 insertions(+), 9 deletions(-) diff --git a/.github/workflows/scl-interoperability-r7.yml b/.github/workflows/scl-interoperability-r7.yml index 1434dbc91..ec15bc0fe 100644 --- a/.github/workflows/scl-interoperability-r7.yml +++ b/.github/workflows/scl-interoperability-r7.yml @@ -48,33 +48,276 @@ jobs: } "ARSAS_COMMIT=$arsasCommit" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append - - name: Resolve isolated R7 engine pin + - name: Resolve current engine while preserving R7/R10 authority shell: pwsh run: | $lock = Get-Content .\ARSAS\engines\ARIEC61850.lock.json -Raw | ConvertFrom-Json $convergence = Get-Content .\ARSAS\evidence\interoperability-reference-target.json -Raw | ConvertFrom-Json + $association = $convergence.postBaselineAcceptance.sclAssociationInteroperability + if ($lock.repository -ne 'masarray/ARIEC61850' -or - $lock.commit -notmatch '^[0-9a-f]{40}$') { - throw 'Invalid ARIEC61850 R7 lock.' + $lock.commit -notmatch '^[0-9a-f]{40} + - name: Checkout exact ARIEC61850 engine revision + shell: pwsh + run: | + git clone --quiet --filter=blob:none --no-checkout "https://github.com/$env:ARIEC61850_REPOSITORY.git" ARIEC61850 + git -C .\ARIEC61850 fetch --quiet --depth 1 origin $env:ARIEC61850_COMMIT + git -C .\ARIEC61850 checkout --quiet --detach $env:ARIEC61850_COMMIT + $actual = (git -C .\ARIEC61850 rev-parse HEAD).Trim() + if ($actual -ne $env:ARIEC61850_COMMIT) { + throw "Engine SHA mismatch. Expected $env:ARIEC61850_COMMIT, got $actual." + } + + - name: Verify canonical SCL interoperability contracts + shell: pwsh + run: | + $canonical = Get-Content .\ARIEC61850\src\AR.Iec61850\Discovery\LiveIedCanonicalModel.cs -Raw + $exporter = Get-Content .\ARIEC61850\src\AR.Iec61850\Scl\Export\CanonicalLiveIedSclExporter.cs -Raw + $association = Get-Content .\ARIEC61850\src\AR.Iec61850\Mms\MmsClientSession.AssociationEvidence.cs -Raw + $cotpClient = Get-Content .\ARIEC61850\src\AR.Iec61850\Osi\CotpClient.cs -Raw + $rcbProjector = Get-Content .\ARIEC61850\src\AR.Iec61850\Scl\Export\LiveRcbLogicalGroupProjector.cs -Raw + $smartDiscovery = Get-Content .\ARIEC61850\src\AR.Iec61850\Mms\MmsClientSession.SmartDiscovery.cs -Raw + $singleFlight = Get-Content .\ARIEC61850\src\AR.Iec61850\Mms\MmsClientSession.SmartDiscoverySingleFlight.cs -Raw + $smartGva = Get-Content .\ARIEC61850\src\AR.Iec61850\Mms\MmsClientSession.SmartVariableAccessAttributes.cs -Raw + $referenceParts = Get-Content .\ARIEC61850\src\AR.Iec61850\Discovery\Iec61850ReferenceParts.cs -Raw + $liveExporter = Get-Content .\ARIEC61850\src\AR.Iec61850\Scl\Export\LiveIedSclExporter.cs -Raw + $standardRegistry = Get-Content .\ARIEC61850\src\AR.Iec61850\Discovery\Iec61850StandardModelRegistry.cs -Raw + $cdcInference = Get-Content .\ARIEC61850\src\AR.Iec61850\Discovery\CdcInferenceEngine.cs -Raw + $consumer = Get-Content .\ARSAS\Services\NativeIec61850Client.CanonicalModel.cs -Raw + $capture = Get-Content .\ARSAS\Services\NativeIec61850Client.SmartDiscoveryCapture.cs -Raw + $convergence = Get-Content .\ARSAS\evidence\interoperability-reference-target.json -Raw | ConvertFrom-Json + $buildTargets = Get-Content .\ARSAS\Directory.Build.targets -Raw + $lifecycle = Get-Content .\ARSAS\Services\NativeIec61850Client.SmartDiscoveryLifecycle.cs -Raw + $sclClient = Get-Content .\ARSAS\Services\NativeIec61850Client.SclAssisted.cs -Raw + $sclPreparation = Get-Content .\ARSAS\Services\SclAssistedConnectionPreparation.cs -Raw + $save = Get-Content .\ARSAS\MainWindow.xaml.cs -Raw + $reloadValidator = Get-Content .\ARSAS\Services\CanonicalSclReloadValidator.cs -Raw + $regression = Get-Content .\ARSAS\tests\ARSAS.Tests\CanonicalLiveSclExportRegressionTests.cs -Raw + $reloadTests = Get-Content .\ARSAS\tests\ARSAS.Tests\CanonicalSclReloadValidatorTests.cs -Raw + + foreach ($required in @( + 'class LiveIedCanonicalModel', + 'InstanceValues', + 'LiveIedCommunicationEvidence')) { + if ($canonical -notmatch [regex]::Escape($required)) { + throw "Canonical engine contract missing: $required" + } + } + + foreach ($required in @( + 'ValidateCanonicalCommunication', + 'PreserveRuntimeServiceCapacity', + 'ApplyCanonicalInstanceValues', + 'ValidateRoundTripAssociation')) { + if ($exporter -notmatch [regex]::Escape($required)) { + throw "Canonical SCL exporter contract missing: $required" + } + } + + if ($association -notmatch 'GetAcceptedCommunicationEvidence') { + throw 'Accepted association evidence API is missing.' + } + + if ($env:ARIEC61850_SOURCE_PR -in @('135', '143')) { + $wireReader = '.\ARIEC61850\src\AR.Iec61850\Acse\AcseAssociationRequestIdentityReader.cs' + if (!(Test-Path $wireReader) -or + $association -notmatch 'AcseAssociationRequestIdentityReader\.Read' -or + $association -notmatch '_cotp\.LastConnectParameters\?\.DestinationTsap' -or + $cotpClient -notmatch 'public CotpConnectParameters\? LastConnectParameters' -or + $cotpClient -notmatch 'LastConnectParameters = acceptedParameters' -or + $association -match 'ApTitle\s*=\s*"1,1,1,999,1"') { + throw 'PR #135 must derive canonical AP/AE/selectors from exact accepted ACSE and COTP session evidence, not duplicated profile constants.' + } + } + + if ($smartDiscovery -notmatch 'DiscoverSmartAsync' -or + $singleFlight -notmatch 'DiscoverSmartSingleFlightAsync' -or + $smartGva -notmatch 'GetVariableAccessAttributesSmartAsync' -or + $referenceParts -notmatch 'instanceStart' -or + $referenceParts -notmatch 'IsFourLetterLnClass' -or + $liveExporter -notmatch 'TryResolveStandardSubDataObjectCdc' -or + $liveExporter -notmatch 'DataObjectReferencePaths' -or + $liveExporter -notmatch 'IsEdition2ServiceTrackingCdc' -or + $standardRegistry -notmatch 'Key\("TCTR", "ARtg"\)' -or + $standardRegistry -notmatch 'Key\("TVTR", "VRtg"\)' -or + $standardRegistry -notmatch 'Key\("LTIM", "TmChgDT"\)' -or + $standardRegistry -notmatch 'Key\("LTRK", "BrcbTrk"\)' -or + $standardRegistry -notmatch 'Key\("XCBR", "EEName"\)' -or + $standardRegistry -notmatch 'Key\("LLN0", "MltLev"\)' -or + $cdcInference -notmatch '"CST".*"BTS".*"UTS".*"STS".*"CTS"' -or + $capture -match 'TryBuildSupplementalGetNameListSnapshotAsync' -or + $capture -match 'DiscoverDomainVariableTypeTreeNamesAsync' -or + $capture -match 'AddAdaptiveLogicalNodeSiblingProbeSignalsAsync' -or + $capture -match 'EnrichEngineeringUnitsAsync') { + throw 'external IEC 61850 reference convergence source contract regressed: smart structure-first discovery or semantic SCL authority is missing, or a forbidden legacy browse path returned.' + } + + if ([int]$convergence.physicalReference.externalReferenceCapture.associations -ne 1 -or + [int]$convergence.physicalReference.rejectedLegacyArsasCapture.associations -ne 2 -or + -not [bool]$convergence.discoveryAcceptance.exactlyOneAssociation -or + -not [bool]$convergence.discoveryAcceptance.supplementalLegacyAssociationForbidden -or + -not [bool]$convergence.sclAcceptance.reopenInArsasRequired -or + -not [bool]$convergence.sclAcceptance.physicalReconnectRequired) { + throw 'external IEC 61850 reference physical acceptance target was weakened.' + } + + if ($consumer -notmatch 'LiveIedCanonicalModelBuilder\.Build\(model, communication, initialRead\)' -or + $capture -match 'InitialFcReadPlanner\.FromSclModel' -or + $capture -match 'ExecuteInitialFcReadPlanSmartAsync' -or + $capture -notmatch 'initialFcRoots=deferred' -or + $buildTargets -notmatch 'VerifyPhysicalProvenSmartDiscoveryRoute' -or + $buildTargets -notmatch '\-VerifyOnly' -or + $buildTargets -match 'GITHUB_WORKFLOW' -or + $buildTargets -match 'SmartDiscoveryProductionPromoted' -or + $lifecycle -notmatch 'PublishCanonicalModel\(model, initialRead\)' -or + $save -notmatch 'CanonicalLiveIedSclExporter\.WriteFiles' -or + $save -notmatch 'profile:\s*"full-model"' -or + $save -notmatch 'CanonicalSclReloadValidator\.Validate' -or + $sclPreparation -notmatch 'MmsLocalAssociationProfile\.ExistingRuntimeDefault' -or + $sclClient -notmatch 'IsSafeTrustedSclInitialReadFc' -or + $sclClient -notmatch 'ExecuteInitialFcReadPlanSmartAsync' -or + $reloadValidator -notmatch 'workspaceService\.Open' -or + $reloadValidator -notmatch 'result\.Profile.*full-model' -or + $reloadValidator -notmatch 'changed the proven calling-side runtime identity' -or + $reloadValidator -notmatch 'reproduce accepted association profile' -or + $regression -notmatch 'SmartDiscovery_DefersEagerFcValueReadsFromStructuralScan' -or + $reloadTests -notmatch 'ExportedCanonicalScl_ReopensThroughArsasWorkspaceWithoutStructuralDrift' -or + $reloadTests -notmatch 'BuildDefaultAssociationPayload' -or + $reloadTests -notmatch 'GoldenRcbShape_RoundTripsThirtyFourRuntimeAsThirtyTwoLogicalWithPhysicalCapacity' -or + $rcbProjector -match 'SameNumericText\(left\.IntegrityPeriodMs, right\.IntegrityPeriodMs\)' -or + $rcbProjector -match 'Same\(left\.OptionalFields, right\.OptionalFields\)') { + throw 'ARSAS canonical discovery -> full-model SCL -> exact native reconnect -> safe initial value-read contract is incomplete.' + } + + - name: Setup .NET 8 + uses: actions/setup-dotnet@v6 + with: + dotnet-version: 8.0.x + + - name: Restore ARSAS solution + run: dotnet restore .\ARSAS\ArIED61850Tester.sln + + - name: Build ARSAS Release + run: dotnet build .\ARSAS\ArIED61850Tester.sln -c Release --no-restore + + - name: Verify R7 smart discovery route was compiled + shell: pwsh + run: | + $consumer = Get-Content .\ARSAS\Services\NativeIec61850Client.cs -Raw + if ($consumer -notmatch 'DiscoverSignalsSmartForCaptureAsync\(cancellationToken, progress\)') { + throw 'R7 build compiled without the smart discovery route; refusing legacy-path artifact.' + } + if ($consumer -notmatch 'if \(SmartDiscoveryCaptureModeEnabled\)') { + throw 'R7 build did not install the smart discovery gate at DiscoverSignalsAsync.' + } + Write-Host 'R7 smart discovery route is installed in the compiled source.' + + - name: Run ARSAS canonical-export regressions + run: dotnet test .\ARSAS\tests\ARSAS.Tests\ARSAS.Tests.csproj -c Release --no-build --no-restore --logger "trx;LogFileName=arsas-r7-scl-tests.trx" --results-directory .\ARSAS\TestResults + + - name: Build engine and run engine tests + shell: pwsh + run: | + dotnet restore .\ARIEC61850\ARIEC61850.sln + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + dotnet build .\ARIEC61850\ARIEC61850.sln -c Release --no-restore + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + dotnet test .\ARIEC61850\ARIEC61850.sln -c Release --no-build --no-restore --logger "trx;LogFileName=ariec61850-r7-scl-tests.trx" --results-directory .\ARSAS\TestResults + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + - name: Publish R7 portable single EXE + shell: pwsh + run: | + .\ARSAS\scripts\publish-windows-portable.ps1 ` + -Version $env:ARSAS_VERSION ` + -Runtime win-x64 ` + -SingleFile $true ` + -SelfContained $true ` + -EngineProject "$env:GITHUB_WORKSPACE\ARIEC61850\src\AR.Iec61850\AR.Iec61850.csproj" ` + -NpcapProject "$env:GITHUB_WORKSPACE\ARIEC61850\src\AR.Iec61850.Transports.Npcap\AR.Iec61850.Transports.Npcap.csproj" + + - name: Smoke test R7 portable executable + shell: pwsh + run: | + $exe = ".\ARSAS\dist\ARSAS-$env:ARSAS_VERSION-win-x64-portable.exe" + if (!(Test-Path $exe -PathType Leaf)) { throw "Portable EXE missing: $exe" } + + $env:DOTNET_BUNDLE_EXTRACT_BASE_DIR = Join-Path $env:RUNNER_TEMP 'ARSAS-r7-scl-bundle-cache' + $process = Start-Process -FilePath $exe -ArgumentList @('--portable-smoke-test') -PassThru + if (-not $process.WaitForExit(30000)) { + Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue + throw 'Portable EXE smoke test timed out.' } - if ($lock.purpose -notmatch 'R7 physical SCL repair pin') { - throw 'Engine lock is not explicitly scoped as the R7 physical SCL repair.' + if ($process.ExitCode -ne 0) { + throw "Portable EXE smoke test failed: $($process.ExitCode)" + } + + $convergencePath = ".\ARSAS\evidence\interoperability-reference-target.json" + $convergenceHash = (Get-FileHash $convergencePath -Algorithm SHA256).Hash.ToLowerInvariant() + $convergence = Get-Content $convergencePath -Raw | ConvertFrom-Json + + @( + 'ARSAS R7 SCL interoperability field-test build', + "ARSAS commit: $env:ARSAS_COMMIT", + "ARIEC61850 commit: $env:ARIEC61850_COMMIT", + "Engine source PR: $env:ARIEC61850_SOURCE_PR", + "Convergence contract SHA256: $convergenceHash", + "Convergence status: $($convergence.status)", + 'Active stack: ARIEC61850 #134 -> #135 -> ARSAS #324', + '', + 'Acceptance target:', + 'IED -> fast structural Smart Discovery -> full-model SCL Ed1/Ed2 -> ARSAS Workspace Reload -> exact native calling/called association -> bounded safe FC-root snapshot.', + 'CI invariant: full-model export excludes no discovered attributes; SCL rebuilds the accepted native handshake; golden RCB shape is 34 runtime -> 32 logical with ConfReportControl max=34.', + 'Physical gate: one MMS association; no supplemental legacy browse; no recursive per-leaf GVA storm; same-relay PCAP + Ed2 IID + Ed1 ICD + diagnostic required before promotion.', + '', + 'This artifact is NOT production-promotion authority and does NOT replace P0-5e/P0-5f physical discovery-budget evidence.' + ) | Set-Content .\ARSAS\dist\R7-SCL-INTEROP-BUILD.txt -Encoding utf8 + + - name: Upload R7 SCL interoperability build + uses: actions/upload-artifact@v7 + with: + name: ARSAS-r7-scl-interoperability-win-x64 + path: | + ARSAS\dist\ARSAS-*-win-x64-portable.exe + ARSAS\dist\R7-SCL-INTEROP-BUILD.txt + ARSAS\evidence\interoperability-reference-target.json + ARSAS\docs\INTEROPERABILITY_REFERENCE_CONTRACT.md + ARSAS\TestResults\*.trx + if-no-files-found: error + retention-days: 14 + -or + $lock.ref -ne 'main') { + throw 'Invalid ARIEC61850 integration lock.' } if ($convergence.status -ne 'physical-retest-passed-merge-ready' -or [int]$convergence.activeStack.enginePerformance.pullRequest -ne 134 -or [int]$convergence.activeStack.engineModelAndScl.pullRequest -ne 135 -or [int]$convergence.activeStack.consumerIntegration.pullRequest -ne 324 -or - $convergence.activeStack.engineModelAndScl.head -ne $lock.commit -or + $convergence.activeStack.engineModelAndScl.head -ne $lock.mergedMainCommit -or + $lock.previousStablePin.commit -ne $lock.mergedMainCommit -or + [int]$lock.previousStablePin.sourcePullRequest -ne 135 -or [bool]$convergence.promotion.productionPromoted -or [bool]$convergence.promotion.mergeAllowedBeforePhysicalRetest -or [bool]$convergence.promotion.physicalRetestRequired -or -not [bool]$convergence.promotion.mergeAllowedAfterPhysicalRetest -or -not [bool]$convergence.promotion.physicalRetestPassed) { - throw 'external IEC 61850 reference convergence single-source-of-truth does not match the physically proven merged-engine + ARSAS #324 authority.' + throw 'Historical R7/R10 convergence authority was weakened.' } - if ([int]$lock.sourcePullRequest -notin @(134, 135)) { - throw "Unexpected R7 engine source PR: $($lock.sourcePullRequest)" + + if ($association.contractId -ne 'SCL-ASSOC-PARTIAL-ADDR-PHYSICAL' -or + $association.status -ne 'physically-accepted-engine-merged-consumer-pending-merge' -or + -not [bool]$association.preservesR10Baseline -or + $association.engine.testedHead -ne $lock.commit -or + [int]$association.engine.pullRequest -ne [int]$lock.sourcePullRequest -or + [int]$lock.sourcePullRequest -ne 143 -or + -not [bool]$association.physicalAcceptance.fullDiscoverySkipped -or + [int]$association.physicalAcceptance.failedReads -ne 0 -or + [int]$association.physicalAcceptance.projectionErrors -ne 0 -or + [int]$association.physicalAcceptance.cacheLoss -ne 0 -or + [int]$association.physicalAcceptance.unresolvedRuntimePoints -ne 0 -or + -not [bool]$association.physicalAcceptance.actualInformationReportObserved) { + throw 'Accepted post-baseline SCL association authority is incomplete or inconsistent.' } "ARIEC61850_REPOSITORY=$($lock.repository)" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append From 4fe6fb3e5da66749c2d2ac12f2ee33c2475c9c56 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Tue, 6 Oct 2026 21:07:30 +0700 Subject: [PATCH 13/14] ci(scl): validate baseline and accepted association layers separately --- .../interoperability-reference-guard.yml | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/.github/workflows/interoperability-reference-guard.yml b/.github/workflows/interoperability-reference-guard.yml index dd5a9d0bc..9b1df9e8f 100644 --- a/.github/workflows/interoperability-reference-guard.yml +++ b/.github/workflows/interoperability-reference-guard.yml @@ -48,6 +48,7 @@ jobs: run: | $lock = Get-Content .\ARSAS\engines\ARIEC61850.lock.json -Raw | ConvertFrom-Json $target = Get-Content .\ARSAS\evidence\interoperability-reference-target.json -Raw | ConvertFrom-Json + $association = $target.postBaselineAcceptance.sclAssociationInteroperability if ($target.status -ne 'physical-retest-passed-merge-ready') { throw "Convergence status changed unexpectedly: $($target.status)" @@ -58,9 +59,31 @@ jobs: [int]$target.activeStack.consumerIntegration.pullRequest -ne 324) { throw 'Merged engine authority must remain PR #134 + PR #135 with ARSAS #324 provenance.' } - if ($lock.commit -ne $target.activeStack.engineModelAndScl.head -or - [int]$lock.sourcePullRequest -ne 135) { - throw 'Engine lock drifted from the convergence single source of truth.' + if ($lock.mergedMainCommit -ne $target.activeStack.engineModelAndScl.head -or + $lock.previousStablePin.commit -ne $lock.mergedMainCommit -or + [int]$lock.previousStablePin.sourcePullRequest -ne 135) { + throw 'Historical engine authority drifted from the R10 convergence single source of truth.' + } + + if ($association.contractId -ne 'SCL-ASSOC-PARTIAL-ADDR-PHYSICAL' -or + $association.status -ne 'physically-accepted-engine-merged-consumer-pending-merge' -or + -not [bool]$association.preservesR10Baseline -or + [int]$association.engine.pullRequest -ne 143 -or + $association.engine.testedHead -ne $lock.commit -or + $association.engine.mergedMain -ne $lock.sclAssociationInteroperability.mergedEngineCommit -or + $association.engine.tree -ne $lock.sclAssociationInteroperability.engineTree -or + [int]$lock.sourcePullRequest -ne 143 -or + $lock.sclAssociationInteroperability.testedEngineCommit -ne $lock.commit -or + -not [bool]$association.physicalAcceptance.fullDiscoverySkipped -or + [int]$association.physicalAcceptance.associationAttempts -ne 1 -or + [int]$association.physicalAcceptance.matchedDomains -ne 5 -or + [int]$association.physicalAcceptance.failedReads -ne 0 -or + [int]$association.physicalAcceptance.projectionErrors -ne 0 -or + [int]$association.physicalAcceptance.cacheLoss -ne 0 -or + [int]$association.physicalAcceptance.unresolvedRuntimePoints -ne 0 -or + [int]$association.physicalAcceptance.cyclicMmsProcessPolling -ne 0 -or + -not [bool]$association.physicalAcceptance.actualInformationReportObserved) { + throw 'Accepted post-baseline SCL association authority drifted or lost physical evidence.' } if ([bool]$target.promotion.productionPromoted -or [bool]$target.promotion.mergeAllowedBeforePhysicalRetest -or From 82e32cae5745085f3c8b0cb1d52ad2ebf20a10d7 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Tue, 6 Oct 2026 21:07:56 +0700 Subject: [PATCH 14/14] ci(scl): layer accepted association authority over R10 readiness --- .../smart-discovery-mainline-readiness.yml | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/.github/workflows/smart-discovery-mainline-readiness.yml b/.github/workflows/smart-discovery-mainline-readiness.yml index 9e4cb87bf..8291c1fb7 100644 --- a/.github/workflows/smart-discovery-mainline-readiness.yml +++ b/.github/workflows/smart-discovery-mainline-readiness.yml @@ -69,17 +69,35 @@ jobs: } } - if ($lock.commit -ne $target.activeStack.engineModelAndScl.mainMerge -or - $lock.commit -ne $r10.testedArtifact.engineMergedMain -or + if ($lock.mergedMainCommit -ne $target.activeStack.engineModelAndScl.mainMerge -or + $lock.mergedMainCommit -ne $r10.testedArtifact.engineMergedMain -or $lock.physicalTestedCommit -ne $r10.testedArtifact.engineTestedHead -or - $lock.mergedMainTree -ne $r10.testedArtifact.engineTree) { - throw 'ARSAS engine lock no longer matches the R10 physical authority.' + $lock.mergedMainTree -ne $r10.testedArtifact.engineTree -or + $lock.previousStablePin.commit -ne $lock.mergedMainCommit -or + [int]$lock.previousStablePin.sourcePullRequest -ne 135) { + throw 'Historical ARSAS engine baseline no longer matches the R10 physical authority.' + } + + $association = $target.postBaselineAcceptance.sclAssociationInteroperability + if ($association.contractId -ne 'SCL-ASSOC-PARTIAL-ADDR-PHYSICAL' -or + $association.status -ne 'physically-accepted-engine-merged-consumer-pending-merge' -or + -not [bool]$association.preservesR10Baseline -or + $association.engine.testedHead -ne $lock.commit -or + [int]$association.engine.pullRequest -ne [int]$lock.sourcePullRequest -or + [int]$lock.sourcePullRequest -ne 143 -or + -not [bool]$association.physicalAcceptance.fullDiscoverySkipped -or + [int]$association.physicalAcceptance.failedReads -ne 0 -or + [int]$association.physicalAcceptance.projectionErrors -ne 0 -or + [int]$association.physicalAcceptance.cacheLoss -ne 0 -or + [int]$association.physicalAcceptance.unresolvedRuntimePoints -ne 0 -or + -not [bool]$association.physicalAcceptance.actualInformationReportObserved) { + throw 'Post-baseline SCL association acceptance is missing or inconsistent.' } "ENGINE_REPOSITORY=$($lock.repository)" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append "ENGINE_COMMIT=$($lock.commit)" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append - - name: Checkout exact merged ARIEC61850 authority + - name: Checkout exact accepted ARIEC61850 integration authority shell: powershell run: | git clone --quiet --filter=blob:none --no-checkout "https://github.com/$env:ENGINE_REPOSITORY.git" ..\ARIEC61850 @@ -87,7 +105,7 @@ jobs: git -C ..\ARIEC61850 checkout --quiet --detach $env:ENGINE_COMMIT $actual = (git -C ..\ARIEC61850 rev-parse HEAD).Trim().ToLowerInvariant() if ($actual -ne $env:ENGINE_COMMIT) { - throw "R10 engine authority mismatch. Expected $env:ENGINE_COMMIT, got $actual." + throw "Accepted engine authority mismatch. Expected $env:ENGINE_COMMIT, got $actual." } - name: Setup .NET 8 @@ -95,7 +113,7 @@ jobs: with: dotnet-version: 8.0.x - - name: Validate and test merged engine authority + - name: Validate and test accepted engine authority shell: powershell run: | dotnet restore ..\ARIEC61850\ARIEC61850.sln