From 59579cc7b7cb75c0d4aee17a4fb6acbe00aa4e5a Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 20 Sep 2026 13:44:35 +0700 Subject: [PATCH 01/13] chore: pin export-only SCL semantic engine candidate --- engines/ARIEC61850.lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/engines/ARIEC61850.lock.json b/engines/ARIEC61850.lock.json index 360e72a96..e53748399 100644 --- a/engines/ARIEC61850.lock.json +++ b/engines/ARIEC61850.lock.json @@ -2,9 +2,9 @@ "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.", + "commit": "e58b42479e46fbbb42a1b17b03a074d8a6fb3b44", + "sourcePullRequest": 140, + "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. SCL export-only semantic candidate PR #140 is pinned only on an isolated ARSAS candidate branch. It starts from the exact physical engine baseline 648124097621046f5f127ceb1cf853fea54db730 and changes no Discovery/MMS/reporting/runtime source; only LiveIedSclExporter plus export-only regression tests differ. Exact live stVal TypeSpecification may refine generic SPS/INS/ENS only while writing SCL; the live discovery model bound to runtime/reporting remains unchanged. PR #139 was physically rejected and closed after AA1E1F06R4 showed slow discovery and BIT STRING/Boolean report rejection with only 23 report-backed rows.", "previousTrialPin": { "commit": "4467124775d8d9d76f3db194f9fbfd97144767a8", "sourcePullRequest": 134, From d888415d140e1c774030cc36a8017adef2b0db83 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 20 Sep 2026 13:44:48 +0700 Subject: [PATCH 02/13] evidence: record rejected semantic runtime candidate and export-only replacement --- .../scl-export-only-semantic-candidate.json | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 evidence/scl-export-only-semantic-candidate.json diff --git a/evidence/scl-export-only-semantic-candidate.json b/evidence/scl-export-only-semantic-candidate.json new file mode 100644 index 000000000..6ffa04e53 --- /dev/null +++ b/evidence/scl-export-only-semantic-candidate.json @@ -0,0 +1,64 @@ +{ + "schemaVersion": 1, + "contractId": "SCL-EXPORT-ONLY-SEMANTIC-P0", + "status": "ci-candidate-physical-retest-pending", + "physicalBaseline": { + "arsasHead": "0d0b9204d6637e3d62e2eee94000386ae43cd0e9", + "arsasLockHead": "6d6c5f6f022b66d93141ace361ed88d4a47ecbb9", + "engineCommit": "648124097621046f5f127ceb1cf853fea54db730", + "relay": "AA1E1F06R4", + "acceptedRuntime": { + "staticMembers": 58, + "liveRows": 58, + "analogRows": 22, + "digitalRows": 36, + "reportBackedRows": 58, + "cyclicMmsProcessPolling": 0 + } + }, + "rejectedCandidate": { + "enginePullRequest": 139, + "engineCommit": "090d81944791be5b690d24342f9262495eda1a09", + "arsasPullRequest": 342, + "arsasCommit": "32dc8542914d069e8fa49fe6ea6574fdaf950564", + "physicalObserved": { + "discoveryRegression": true, + "reportBackedRowsObserved": 23, + "repeatedBooleanBitStringRejections": true + }, + "disposition": "physically-rejected-closed" + }, + "replacementCandidate": { + "enginePullRequest": 140, + "engineCommit": "e58b42479e46fbbb42a1b17b03a074d8a6fb3b44", + "engineBase": "648124097621046f5f127ceb1cf853fea54db730", + "allowedEngineDiff": [ + "src/AR.Iec61850/Scl/Export/LiveIedSclExporter.cs", + "tests/AR.Iec61850.Tests/Scl/LiveIedSclExportOnlySemanticAuthorityTests.cs" + ], + "discoveryRuntimeSourceMustMatchPhysicalBaseline": true, + "liveDiscoveryModelMutationAllowed": false, + "acquisitionBehaviorChangeAllowed": false + }, + "semanticTarget": { + "object": "CBClsCounter", + "liveRuntimeCdcRemains": "SPS", + "exportedSclCdc": "INS", + "exportedStValBType": "INT32", + "reopenedSclCdc": "INS", + "reopenedStValBType": "INT32" + }, + "physicalAcceptance": { + "discoveryMustMatchAcceptedBaseline": true, + "staticMembers": 58, + "liveRows": 58, + "analogRows": 22, + "digitalRows": 36, + "reportBackedRows": 58, + "actualInformationReportRequired": true, + "cyclicMmsProcessPolling": 0, + "saveSclWhileMonitoringMustRemainNonDisruptive": true, + "reopenedSclLiveRows": 58, + "noBooleanBitStringRejectionRegression": true + } +} From 8e2cad268fd608d7005bdc9eaac08c151d6f8f7c Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 20 Sep 2026 13:45:03 +0700 Subject: [PATCH 03/13] tests: lock export-only semantic isolation --- .../P07ReleaseCandidateLockRegressionTests.cs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/tests/ARSAS.Tests/P07ReleaseCandidateLockRegressionTests.cs b/tests/ARSAS.Tests/P07ReleaseCandidateLockRegressionTests.cs index fc57090c3..4010f4f6b 100644 --- a/tests/ARSAS.Tests/P07ReleaseCandidateLockRegressionTests.cs +++ b/tests/ARSAS.Tests/P07ReleaseCandidateLockRegressionTests.cs @@ -72,19 +72,32 @@ public void LockFile_FreezesThePhysicallyAcceptedRuntimeBaselineAndOpenSclSemant } [Fact] - public void CurrentEngineLock_RemainsOnThePhysicalBaselineUntilSemanticCandidateIsExplicitlyIntegrated() + public void ExportOnlyCandidate_PreservesPhysicalEngineAuthorityAndPinsOnlyTheIsolatedExporterRevision() { using var baseline = JsonDocument.Parse( File.ReadAllText(FindRepoFile("evidence/p0.7-release-candidate-lock.json"))); + using var candidate = JsonDocument.Parse( + File.ReadAllText(FindRepoFile("evidence/scl-export-only-semantic-candidate.json"))); using var engine = JsonDocument.Parse( File.ReadAllText(FindRepoFile("engines/ARIEC61850.lock.json"))); - var expected = baseline.RootElement + var physical = baseline.RootElement .GetProperty("physicalReference") .GetProperty("engineBaseline") .GetString(); + var replacement = candidate.RootElement.GetProperty("replacementCandidate"); - Assert.Equal(expected, engine.RootElement.GetProperty("commit").GetString()); + Assert.Equal("648124097621046f5f127ceb1cf853fea54db730", physical); + Assert.Equal(140, replacement.GetProperty("enginePullRequest").GetInt32()); + Assert.Equal( + "e58b42479e46fbbb42a1b17b03a074d8a6fb3b44", + replacement.GetProperty("engineCommit").GetString()); + Assert.Equal( + replacement.GetProperty("engineCommit").GetString(), + engine.RootElement.GetProperty("commit").GetString()); + Assert.True(replacement.GetProperty("discoveryRuntimeSourceMustMatchPhysicalBaseline").GetBoolean()); + Assert.False(replacement.GetProperty("liveDiscoveryModelMutationAllowed").GetBoolean()); + Assert.False(replacement.GetProperty("acquisitionBehaviorChangeAllowed").GetBoolean()); } [Theory] From d05e42bf17b4109bd5cb9d9578b5c9b8c3401b7e Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 20 Sep 2026 13:45:19 +0700 Subject: [PATCH 04/13] tests: retain physical provenance under export-only pin --- .../CanonicalLiveSclExportRegressionTests.cs | 49 ++++++++++++------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs b/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs index 67364653b..0370501de 100644 --- a/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs +++ b/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs @@ -396,39 +396,54 @@ public void SourceClean_GuardsApprovedFirstPartyConvergenceAuthorities() [Fact] - public void EnginePin_MatchesPhysicalSclRepairHead() + public void EnginePin_UsesExportOnlyCandidateWhileRetainingPhysicalBaselineProvenance() { var lockFile = File.ReadAllText(FindRepoFile("engines/ARIEC61850.lock.json")); + var candidate = File.ReadAllText( + FindRepoFile("evidence/scl-export-only-semantic-candidate.json")); Assert.Contains( - "\"commit\": \"648124097621046f5f127ceb1cf853fea54db730\"", + "\"commit\": \"e58b42479e46fbbb42a1b17b03a074d8a6fb3b44\"", lockFile, StringComparison.Ordinal); + Assert.Contains("\"sourcePullRequest\": 140", lockFile, StringComparison.Ordinal); + + Assert.Contains( + "\"engineCommit\": \"648124097621046f5f127ceb1cf853fea54db730\"", + candidate, + StringComparison.Ordinal); + Assert.Contains( + "\"engineBase\": \"648124097621046f5f127ceb1cf853fea54db730\"", + candidate, + StringComparison.Ordinal); + Assert.Contains( + "\"discoveryRuntimeSourceMustMatchPhysicalBaseline\": true", + candidate, + StringComparison.Ordinal); + Assert.Contains( + "\"liveDiscoveryModelMutationAllowed\": false", + candidate, + StringComparison.Ordinal); + Assert.Contains( + "\"acquisitionBehaviorChangeAllowed\": false", + candidate, + StringComparison.Ordinal); + Assert.Contains( "\"physicalTestedCommit\": \"9935d6902d786cc69b299260fe36b835944d5e81\"", lockFile, StringComparison.Ordinal); - Assert.Contains("\"sourcePullRequest\": 135", lockFile, StringComparison.Ordinal); + Assert.Contains( + "\"mergedMainCommit\": \"648124097621046f5f127ceb1cf853fea54db730\"", + 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); - Assert.Contains("runtime-mutable", lockFile, StringComparison.OrdinalIgnoreCase); - Assert.Contains("DataSet/ConfRev/domain/LN/buffered identity", lockFile, StringComparison.Ordinal); Assert.Contains("full-model SCL", lockFile, StringComparison.Ordinal); - Assert.Contains("CDC-aware WYE/DEL/SEQ SDO", lockFile, StringComparison.Ordinal); - Assert.Contains("FC ownership", lockFile, StringComparison.Ordinal); - Assert.Contains("TCTR/TVTR/LTIM/EEName/MltLev", lockFile, StringComparison.Ordinal); - Assert.Contains("LTRK service-tracking", lockFile, StringComparison.Ordinal); - Assert.Contains("Edition-1 schema downgrade protection", lockFile, StringComparison.Ordinal); Assert.Contains("TypeSpecification declaration order", lockFile, StringComparison.Ordinal); - Assert.Contains("SG/SE as setting data", lockFile, StringComparison.Ordinal); - Assert.Contains("MHAI THD phase groups as WYE/CMV", lockFile, StringComparison.Ordinal); Assert.Contains("bounded FC-read policy", lockFile, StringComparison.Ordinal); - Assert.Contains("complete PR #134 smart-discovery performance head", lockFile, StringComparison.Ordinal); Assert.Contains("Production promotion remains fail-closed", lockFile, StringComparison.Ordinal); - Assert.Contains( - "\"commit\": \"4467124775d8d9d76f3db194f9fbfd97144767a8\"", - lockFile, - StringComparison.Ordinal); + Assert.Contains("SCL export-only semantic candidate PR #140", lockFile, StringComparison.Ordinal); } From e2256d59ffaef6d02c2577274b004cff5d508141 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 20 Sep 2026 13:45:35 +0700 Subject: [PATCH 05/13] tests: prove SCL export cannot mutate live runtime semantics --- ...ortOnlySemanticIsolationRegressionTests.cs | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 tests/ARSAS.Tests/SclExportOnlySemanticIsolationRegressionTests.cs diff --git a/tests/ARSAS.Tests/SclExportOnlySemanticIsolationRegressionTests.cs b/tests/ARSAS.Tests/SclExportOnlySemanticIsolationRegressionTests.cs new file mode 100644 index 000000000..69f4e88ba --- /dev/null +++ b/tests/ARSAS.Tests/SclExportOnlySemanticIsolationRegressionTests.cs @@ -0,0 +1,158 @@ +using System.Xml.Linq; +using AR.Iec61850.Discovery; +using AR.Iec61850.Scl.Engineering; +using AR.Iec61850.Scl.Export; + +namespace ARSAS.Tests; + +public sealed class SclExportOnlySemanticIsolationRegressionTests +{ + [Fact] + public void SaveScl_CorrectsCounterMetadataWithoutMutatingTheLiveRuntimeModel() + { + var model = BuildModel(); + + var liveCounter = Assert.Single( + model.LogicalDevices + .SelectMany(device => device.LogicalNodes) + .SelectMany(node => node.DataObjects)); + Assert.Equal("SPS", liveCounter.InferredCdc); + + var document = LiveIedSclExporter.BuildDocument( + model, + new LiveIedSclExportOptions { Profile = "full-model" }); + + var ns = document.Root!.Name.Namespace; + var lNodeType = Assert.Single( + document.Descendants(ns + "LNodeType"), + element => (string?)element.Attribute("lnClass") == "GGIO"); + var dataObject = Assert.Single( + lNodeType.Elements(ns + "DO"), + element => (string?)element.Attribute("name") == "CBClsCounter"); + var doTypeId = (string?)dataObject.Attribute("type") ?? string.Empty; + var doType = Assert.Single( + document.Descendants(ns + "DOType"), + element => (string?)element.Attribute("id") == doTypeId); + + Assert.Equal("INS", (string?)doType.Attribute("cdc")); + Assert.Equal( + "INT32", + (string?)Assert.Single( + doType.Elements(ns + "DA"), + element => (string?)element.Attribute("name") == "stVal") + .Attribute("bType")); + + Assert.Equal("SPS", liveCounter.InferredCdc); + + var reopened = SclLiveModelProjectionBuilder.Build(document, "generated.iid"); + var reopenedCounter = Assert.Single( + reopened.LogicalDevices + .SelectMany(device => device.LogicalNodes) + .SelectMany(node => node.DataObjects), + item => item.Name == "CBClsCounter"); + + Assert.Equal("INS", reopenedCounter.InferredCdc); + Assert.Equal( + "INT32", + Assert.Single( + reopenedCounter.Attributes, + attribute => attribute.AttributePath == "stVal").SclBType); + } + + [Fact] + public void CandidateEvidence_RequiresZeroDiscoveryRuntimeSourceDrift() + { + using var document = System.Text.Json.JsonDocument.Parse( + File.ReadAllText(FindRepoFile("evidence/scl-export-only-semantic-candidate.json"))); + var replacement = document.RootElement.GetProperty("replacementCandidate"); + + Assert.True(replacement.GetProperty("discoveryRuntimeSourceMustMatchPhysicalBaseline").GetBoolean()); + Assert.False(replacement.GetProperty("liveDiscoveryModelMutationAllowed").GetBoolean()); + Assert.False(replacement.GetProperty("acquisitionBehaviorChangeAllowed").GetBoolean()); + + var allowed = replacement.GetProperty("allowedEngineDiff") + .EnumerateArray() + .Select(item => item.GetString()) + .ToArray(); + Assert.Equal(2, allowed.Length); + Assert.Contains("src/AR.Iec61850/Scl/Export/LiveIedSclExporter.cs", allowed); + Assert.Contains("tests/AR.Iec61850.Tests/Scl/LiveIedSclExportOnlySemanticAuthorityTests.cs", allowed); + } + + private static LiveIedModelDiscoveryDocument BuildModel() + => new() + { + IedName = "IED", + AccessPointName = "AP1", + LogicalDevices = + [ + new LiveIedLogicalDeviceModel + { + MmsDomain = "IEDADD", + Inst = "ADD", + LogicalNodes = + [ + new LiveIedLogicalNodeModel + { + Name = "GGIO1", + LnClass = "GGIO", + LnInst = "1", + ProposedLnTypeId = "LN_GGIO_GGIO1", + DataObjects = + [ + new LiveIedDataObjectModel + { + Reference = "IEDADD/GGIO1.CBClsCounter", + Name = "CBClsCounter", + ProposedDoTypeId = "DO_SPS_GGIO_CBClsCounter", + InferredCdc = "SPS", + CdcConfidence = 0.78, + ConfidenceLevel = LiveIedDiscoveryConfidenceLevel.Medium, + Attributes = + [ + Attribute("stVal", "INT32", "integer"), + Attribute("q", "Quality", "bit-string"), + Attribute("t", "Timestamp", "utc-time") + ] + } + ] + } + ] + } + ] + }; + + private static LiveIedDataAttributeModel Attribute( + string path, + string sclBType, + string mmsType) + => new() + { + ObjectReference = "IEDADD/GGIO1.CBClsCounter." + path, + AttributePath = path, + FunctionalConstraint = "ST", + MmsReference = "IEDADD/GGIO1$ST$CBClsCounter$" + path, + MmsItemName = "GGIO1$ST$CBClsCounter$" + path, + Source = "GetVariableAccessAttributes", + SclBType = sclBType, + MmsType = mmsType, + TypeDiscoveryStatus = "Exact", + TypeSource = "GetVariableAccessAttributes", + TypeConfidence = LiveIedDiscoveryConfidenceLevel.Exact, + FunctionalConstraintConfidence = LiveIedDiscoveryConfidenceLevel.Exact + }; + + private static string FindRepoFile(string relativePath) + { + DirectoryInfo? directory = new(AppContext.BaseDirectory); + while (directory != null) + { + var candidate = Path.Combine(directory.FullName, relativePath); + if (File.Exists(candidate)) + return candidate; + directory = directory.Parent; + } + + throw new FileNotFoundException(relativePath); + } +} From a9868beacba87eaf938d9a511dffa0d7a1a25465 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 20 Sep 2026 13:46:00 +0700 Subject: [PATCH 06/13] ci: require byte-identical acquisition source for SCL candidate --- .../scl-export-only-semantic-candidate.yml | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 .github/workflows/scl-export-only-semantic-candidate.yml diff --git a/.github/workflows/scl-export-only-semantic-candidate.yml b/.github/workflows/scl-export-only-semantic-candidate.yml new file mode 100644 index 000000000..c7edc74f1 --- /dev/null +++ b/.github/workflows/scl-export-only-semantic-candidate.yml @@ -0,0 +1,166 @@ +name: SCL Export-Only Semantic Candidate + +on: + pull_request: + paths: + - "engines/ARIEC61850.lock.json" + - "evidence/scl-export-only-semantic-candidate.json" + - "tests/ARSAS.Tests/P07ReleaseCandidateLockRegressionTests.cs" + - "tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs" + - "tests/ARSAS.Tests/SclExportOnlySemanticIsolationRegressionTests.cs" + - ".github/workflows/scl-export-only-semantic-candidate.yml" + workflow_dispatch: + +concurrency: + group: scl-export-only-semantic-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + build-export-only-candidate: + runs-on: windows-latest + steps: + - name: Checkout exact ARSAS candidate + shell: pwsh + env: + ARSAS_SOURCE_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: | + git clone --quiet --filter=blob:none --no-checkout "https://github.com/$env:GITHUB_REPOSITORY.git" ARSAS + git -C .\ARSAS fetch --quiet --depth 1 origin $env:ARSAS_SOURCE_SHA + git -C .\ARSAS checkout --quiet --detach $env:ARSAS_SOURCE_SHA + $actual = (git -C .\ARSAS rev-parse HEAD).Trim().ToLowerInvariant() + if ($actual -ne $env:ARSAS_SOURCE_SHA.ToLowerInvariant()) { throw "ARSAS SHA mismatch." } + "ARSAS_COMMIT=$actual" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append + + - name: Validate candidate contract + shell: pwsh + run: | + $baseline = Get-Content .\ARSAS\evidence\p0.7-release-candidate-lock.json -Raw | ConvertFrom-Json + $candidate = Get-Content .\ARSAS\evidence\scl-export-only-semantic-candidate.json -Raw | ConvertFrom-Json + $lock = Get-Content .\ARSAS\engines\ARIEC61850.lock.json -Raw | ConvertFrom-Json + + if ($baseline.physicalReference.engineBaseline -ne '648124097621046f5f127ceb1cf853fea54db730' -or + [int]$baseline.acceptedRuntime.staticMembers -ne 58 -or + [int]$baseline.acceptedRuntime.selectedLiveRows -ne 58 -or + [int]$baseline.acceptedRuntime.analogRows -ne 22 -or + [int]$baseline.acceptedRuntime.digitalRows -ne 36 -or + [int]$baseline.acceptedRuntime.reportBackedRows -ne 58 -or + [int]$baseline.acceptedRuntime.cyclicMmsProcessPolling -ne 0) { + throw 'Frozen P0.7 baseline changed.' + } + + if ($candidate.contractId -ne 'SCL-EXPORT-ONLY-SEMANTIC-P0' -or + [int]$candidate.replacementCandidate.enginePullRequest -ne 140 -or + $candidate.replacementCandidate.engineBase -ne '648124097621046f5f127ceb1cf853fea54db730' -or + $candidate.replacementCandidate.engineCommit -ne $lock.commit -or + -not [bool]$candidate.replacementCandidate.discoveryRuntimeSourceMustMatchPhysicalBaseline -or + [bool]$candidate.replacementCandidate.liveDiscoveryModelMutationAllowed -or + [bool]$candidate.replacementCandidate.acquisitionBehaviorChangeAllowed) { + throw 'Export-only semantic candidate contract invalid.' + } + + if ([int]$lock.sourcePullRequest -ne 140) { throw 'Only engine PR #140 is permitted.' } + "ARIEC61850_REPOSITORY=$($lock.repository)" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append + "ARIEC61850_COMMIT=$($lock.commit)" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append + $version = (Get-Content .\ARSAS\VERSION -Raw).Trim() + "ARSAS_VERSION=$version" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append + + - name: Checkout exact engine candidate and baseline + 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 fetch --quiet --depth 1 origin 648124097621046f5f127ceb1cf853fea54db730 + git -C .\ARIEC61850 checkout --quiet --detach $env:ARIEC61850_COMMIT + + - name: Prove engine is export-only + shell: pwsh + run: | + $base = '648124097621046f5f127ceb1cf853fea54db730' + $head = $env:ARIEC61850_COMMIT + $changed = @(git -C .\ARIEC61850 diff --name-only $base $head) + $expected = @( + 'src/AR.Iec61850/Scl/Export/LiveIedSclExporter.cs', + 'tests/AR.Iec61850.Tests/Scl/LiveIedSclExportOnlySemanticAuthorityTests.cs' + ) + $unexpected = @($changed | Where-Object { $_ -notin $expected }) + $missing = @($expected | Where-Object { $_ -notin $changed }) + if ($unexpected.Count -gt 0 -or $missing.Count -gt 0 -or $changed.Count -ne 2) { + $changed | ForEach-Object { Write-Host $_ } + throw 'Engine candidate is not export-only.' + } + + git -C .\ARIEC61850 diff --exit-code $base $head -- src/AR.Iec61850/Discovery src/AR.Iec61850/Mms src/AR.Iec61850/Osi src/AR.Iec61850/Acse + if ($LASTEXITCODE -ne 0) { throw 'Discovery/wire source differs from physical baseline.' } + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Build and test engine + 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=engine-export-only.trx" --results-directory .\ARSAS\TestResults + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + - name: Build and test ARSAS + shell: pwsh + run: | + dotnet restore .\ARSAS\ArIED61850Tester.sln + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + dotnet build .\ARSAS\ArIED61850Tester.sln -c Release --no-restore + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + dotnet test .\ARSAS\tests\ARSAS.Tests\ARSAS.Tests.csproj -c Release --no-build --no-restore --logger "trx;LogFileName=arsas-export-only.trx" --results-directory .\ARSAS\TestResults + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + - name: Publish portable candidate + 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 and manifest + shell: pwsh + run: | + $exe = ".\ARSAS\dist\ARSAS-$env:ARSAS_VERSION-win-x64-portable.exe" + if (!(Test-Path $exe -PathType Leaf)) { throw "Portable EXE missing." } + $env:DOTNET_BUNDLE_EXTRACT_BASE_DIR = Join-Path $env:RUNNER_TEMP 'ARSAS-export-only-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 ($process.ExitCode -ne 0) { throw "Portable smoke test failed." } + + @( + 'ARSAS SCL export-only semantic physical-retest candidate', + "ARSAS commit: $env:ARSAS_COMMIT", + "Engine candidate: $env:ARIEC61850_COMMIT", + 'Physical engine baseline: 648124097621046f5f127ceb1cf853fea54db730', + 'Engine diff is exactly LiveIedSclExporter.cs plus one export-only regression test.', + 'Discovery/MMS/Osi/Acse source is byte-for-byte identical to the physical baseline.', + 'Acceptance: discovery path/speed baseline, 58/58, analog 22, digital 36, report-backed 58, actual InformationReport, polling 0.', + 'SCL target: CBClsCounter live runtime unchanged; saved/reopened SCL INS/INT32.' + ) | Set-Content .\ARSAS\dist\SCL-EXPORT-ONLY-CANDIDATE.txt -Encoding utf8 + + - name: Upload candidate + uses: actions/upload-artifact@v4 + with: + name: ARSAS-scl-export-only-semantic-win-x64 + path: | + ARSAS\dist\ARSAS-*-win-x64-portable.exe + ARSAS\dist\SCL-EXPORT-ONLY-CANDIDATE.txt + ARSAS\evidence\p0.7-release-candidate-lock.json + ARSAS\evidence\scl-export-only-semantic-candidate.json + ARSAS\TestResults\*.trx + if-no-files-found: error + retention-days: 14 From bb3959f09c5e7a33d8067b7f7a7951752bdd17fc Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 20 Sep 2026 13:47:07 +0700 Subject: [PATCH 07/13] chore: advance export-only engine pin after comment cleanup --- 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 e53748399..054314b48 100644 --- a/engines/ARIEC61850.lock.json +++ b/engines/ARIEC61850.lock.json @@ -2,9 +2,9 @@ "schemaVersion": 1, "repository": "masarray/ARIEC61850", "ref": "main", - "commit": "e58b42479e46fbbb42a1b17b03a074d8a6fb3b44", + "commit": "32fae3a56b1e21874895c7e781923f324cd1e54e", "sourcePullRequest": 140, - "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. SCL export-only semantic candidate PR #140 is pinned only on an isolated ARSAS candidate branch. It starts from the exact physical engine baseline 648124097621046f5f127ceb1cf853fea54db730 and changes no Discovery/MMS/reporting/runtime source; only LiveIedSclExporter plus export-only regression tests differ. Exact live stVal TypeSpecification may refine generic SPS/INS/ENS only while writing SCL; the live discovery model bound to runtime/reporting remains unchanged. PR #139 was physically rejected and closed after AA1E1F06R4 showed slow discovery and BIT STRING/Boolean report rejection with only 23 report-backed rows.", + "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. SCL export-only semantic candidate PR #140 is pinned only on an isolated ARSAS candidate branch. It starts from the exact physical engine baseline 648124097621046f5f127ceb1cf853fea54db730 and changes no Discovery/MMS/reporting/runtime source; only LiveIedSclExporter plus export-only regression tests differ. Exact live stVal TypeSpecification may refine generic SPS/INS/ENS only while writing SCL; the live discovery model bound to runtime/reporting remains unchanged. PR #139 was physically rejected and closed after AA1E1F06R4 showed slow discovery and BIT STRING/Boolean report rejection with only 23 report-backed rows. The only additional Discovery-tree delta is two source-clean wording comments in Iec61850StandardModelRegistry.cs; executable registry logic is unchanged.", "previousTrialPin": { "commit": "4467124775d8d9d76f3db194f9fbfd97144767a8", "sourcePullRequest": 134, From d88f64c97bd7c8e1e7373f093ab8b5313c847c0d Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 20 Sep 2026 13:47:18 +0700 Subject: [PATCH 08/13] evidence: record comment-only discovery delta --- evidence/scl-export-only-semantic-candidate.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/evidence/scl-export-only-semantic-candidate.json b/evidence/scl-export-only-semantic-candidate.json index 6ffa04e53..34b2f547c 100644 --- a/evidence/scl-export-only-semantic-candidate.json +++ b/evidence/scl-export-only-semantic-candidate.json @@ -30,15 +30,17 @@ }, "replacementCandidate": { "enginePullRequest": 140, - "engineCommit": "e58b42479e46fbbb42a1b17b03a074d8a6fb3b44", + "engineCommit": "32fae3a56b1e21874895c7e781923f324cd1e54e", "engineBase": "648124097621046f5f127ceb1cf853fea54db730", "allowedEngineDiff": [ "src/AR.Iec61850/Scl/Export/LiveIedSclExporter.cs", - "tests/AR.Iec61850.Tests/Scl/LiveIedSclExportOnlySemanticAuthorityTests.cs" + "tests/AR.Iec61850.Tests/Scl/LiveIedSclExportOnlySemanticAuthorityTests.cs", + "src/AR.Iec61850/Discovery/Iec61850StandardModelRegistry.cs" ], "discoveryRuntimeSourceMustMatchPhysicalBaseline": true, "liveDiscoveryModelMutationAllowed": false, - "acquisitionBehaviorChangeAllowed": false + "acquisitionBehaviorChangeAllowed": false, + "commentOnlyDiscoveryDelta": "Iec61850StandardModelRegistry.cs has only two comment wording replacements required by source-clean; executable registry logic is unchanged." }, "semanticTarget": { "object": "CBClsCounter", From 965c3bca2392a35e69f9dc9e1e6c725de84f8196 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 20 Sep 2026 13:47:26 +0700 Subject: [PATCH 09/13] tests: update isolated exporter engine pin --- tests/ARSAS.Tests/P07ReleaseCandidateLockRegressionTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ARSAS.Tests/P07ReleaseCandidateLockRegressionTests.cs b/tests/ARSAS.Tests/P07ReleaseCandidateLockRegressionTests.cs index 4010f4f6b..fe1de2c41 100644 --- a/tests/ARSAS.Tests/P07ReleaseCandidateLockRegressionTests.cs +++ b/tests/ARSAS.Tests/P07ReleaseCandidateLockRegressionTests.cs @@ -90,7 +90,7 @@ public void ExportOnlyCandidate_PreservesPhysicalEngineAuthorityAndPinsOnlyTheIs Assert.Equal("648124097621046f5f127ceb1cf853fea54db730", physical); Assert.Equal(140, replacement.GetProperty("enginePullRequest").GetInt32()); Assert.Equal( - "e58b42479e46fbbb42a1b17b03a074d8a6fb3b44", + "32fae3a56b1e21874895c7e781923f324cd1e54e", replacement.GetProperty("engineCommit").GetString()); Assert.Equal( replacement.GetProperty("engineCommit").GetString(), From f100fb7583f43638212b50dcb54649389fa82478 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 20 Sep 2026 13:47:39 +0700 Subject: [PATCH 10/13] tests: update export-only engine candidate --- tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs b/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs index 0370501de..6d4c8d742 100644 --- a/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs +++ b/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs @@ -403,7 +403,7 @@ public void EnginePin_UsesExportOnlyCandidateWhileRetainingPhysicalBaselineProve FindRepoFile("evidence/scl-export-only-semantic-candidate.json")); Assert.Contains( - "\"commit\": \"e58b42479e46fbbb42a1b17b03a074d8a6fb3b44\"", + "\"commit\": \"32fae3a56b1e21874895c7e781923f324cd1e54e\"", lockFile, StringComparison.Ordinal); Assert.Contains("\"sourcePullRequest\": 140", lockFile, StringComparison.Ordinal); From e561486c0dd436d4ce6dedaba5805a479a38a36a Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 20 Sep 2026 13:47:43 +0700 Subject: [PATCH 11/13] tests: record source-clean comment-only delta --- .../SclExportOnlySemanticIsolationRegressionTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ARSAS.Tests/SclExportOnlySemanticIsolationRegressionTests.cs b/tests/ARSAS.Tests/SclExportOnlySemanticIsolationRegressionTests.cs index 69f4e88ba..fd4df0634 100644 --- a/tests/ARSAS.Tests/SclExportOnlySemanticIsolationRegressionTests.cs +++ b/tests/ARSAS.Tests/SclExportOnlySemanticIsolationRegressionTests.cs @@ -74,9 +74,9 @@ public void CandidateEvidence_RequiresZeroDiscoveryRuntimeSourceDrift() .EnumerateArray() .Select(item => item.GetString()) .ToArray(); - Assert.Equal(2, allowed.Length); + Assert.Equal(3, allowed.Length); Assert.Contains("src/AR.Iec61850/Scl/Export/LiveIedSclExporter.cs", allowed); - Assert.Contains("tests/AR.Iec61850.Tests/Scl/LiveIedSclExportOnlySemanticAuthorityTests.cs", allowed); + Assert.Contains("tests/AR.Iec61850.Tests/Scl/LiveIedSclExportOnlySemanticAuthorityTests.cs", allowed);\n Assert.Contains("src/AR.Iec61850/Discovery/Iec61850StandardModelRegistry.cs", allowed); } private static LiveIedModelDiscoveryDocument BuildModel() From a9e14a6a51520fbb45f59733e21e183af6c9dcea Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 20 Sep 2026 13:47:59 +0700 Subject: [PATCH 12/13] fix: repair candidate regression source formatting --- .../SclExportOnlySemanticIsolationRegressionTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/ARSAS.Tests/SclExportOnlySemanticIsolationRegressionTests.cs b/tests/ARSAS.Tests/SclExportOnlySemanticIsolationRegressionTests.cs index fd4df0634..f74b8c71c 100644 --- a/tests/ARSAS.Tests/SclExportOnlySemanticIsolationRegressionTests.cs +++ b/tests/ARSAS.Tests/SclExportOnlySemanticIsolationRegressionTests.cs @@ -76,7 +76,8 @@ public void CandidateEvidence_RequiresZeroDiscoveryRuntimeSourceDrift() .ToArray(); Assert.Equal(3, allowed.Length); Assert.Contains("src/AR.Iec61850/Scl/Export/LiveIedSclExporter.cs", allowed); - Assert.Contains("tests/AR.Iec61850.Tests/Scl/LiveIedSclExportOnlySemanticAuthorityTests.cs", allowed);\n Assert.Contains("src/AR.Iec61850/Discovery/Iec61850StandardModelRegistry.cs", allowed); + Assert.Contains("tests/AR.Iec61850.Tests/Scl/LiveIedSclExportOnlySemanticAuthorityTests.cs", allowed); + Assert.Contains("src/AR.Iec61850/Discovery/Iec61850StandardModelRegistry.cs", allowed); } private static LiveIedModelDiscoveryDocument BuildModel() From 6306f63203a9b565212f7471e74d0d45211aa5e9 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 20 Sep 2026 13:48:28 +0700 Subject: [PATCH 13/13] ci: permit only verified comment-only registry cleanup --- .../scl-export-only-semantic-candidate.yml | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/.github/workflows/scl-export-only-semantic-candidate.yml b/.github/workflows/scl-export-only-semantic-candidate.yml index c7edc74f1..efa210f01 100644 --- a/.github/workflows/scl-export-only-semantic-candidate.yml +++ b/.github/workflows/scl-export-only-semantic-candidate.yml @@ -80,17 +80,30 @@ jobs: $changed = @(git -C .\ARIEC61850 diff --name-only $base $head) $expected = @( 'src/AR.Iec61850/Scl/Export/LiveIedSclExporter.cs', - 'tests/AR.Iec61850.Tests/Scl/LiveIedSclExportOnlySemanticAuthorityTests.cs' + 'tests/AR.Iec61850.Tests/Scl/LiveIedSclExportOnlySemanticAuthorityTests.cs', + 'src/AR.Iec61850/Discovery/Iec61850StandardModelRegistry.cs' ) $unexpected = @($changed | Where-Object { $_ -notin $expected }) $missing = @($expected | Where-Object { $_ -notin $changed }) - if ($unexpected.Count -gt 0 -or $missing.Count -gt 0 -or $changed.Count -ne 2) { + if ($unexpected.Count -gt 0 -or $missing.Count -gt 0 -or $changed.Count -ne 3) { $changed | ForEach-Object { Write-Host $_ } - throw 'Engine candidate is not export-only.' + throw 'Engine candidate is not export-only plus the approved comment cleanup.' } - git -C .\ARIEC61850 diff --exit-code $base $head -- src/AR.Iec61850/Discovery src/AR.Iec61850/Mms src/AR.Iec61850/Osi src/AR.Iec61850/Acse - if ($LASTEXITCODE -ne 0) { throw 'Discovery/wire source differs from physical baseline.' } + $registryDiff = @(git -C .\ARIEC61850 diff --unified=0 $base $head -- src/AR.Iec61850/Discovery/Iec61850StandardModelRegistry.cs) + $registryChangedLines = @($registryDiff | Where-Object { + $_ -match '^[+-]' -and $_ -notmatch '^(---|\+\+\+)' + }) + $registryCodeDelta = @($registryChangedLines | Where-Object { + $_ -notmatch '^[+-]\s*//' + }) + if ($registryCodeDelta.Count -ne 0) { + $registryCodeDelta | ForEach-Object { Write-Host $_ } + throw 'StandardModelRegistry contains executable code drift; only source-clean comment wording is allowed.' + } + + git -C .\ARIEC61850 diff --exit-code $base $head -- src/AR.Iec61850/Mms src/AR.Iec61850/Osi src/AR.Iec61850/Acse + if ($LASTEXITCODE -ne 0) { throw 'Wire/MMS source differs from physical baseline.' } - name: Setup .NET uses: actions/setup-dotnet@v4 @@ -146,8 +159,8 @@ jobs: "ARSAS commit: $env:ARSAS_COMMIT", "Engine candidate: $env:ARIEC61850_COMMIT", 'Physical engine baseline: 648124097621046f5f127ceb1cf853fea54db730', - 'Engine diff is exactly LiveIedSclExporter.cs plus one export-only regression test.', - 'Discovery/MMS/Osi/Acse source is byte-for-byte identical to the physical baseline.', + 'Engine executable diff is LiveIedSclExporter.cs only; one regression test plus two source-clean comment lines are the only additional changes.', + 'Discovery executable logic plus MMS/Osi/Acse source matches the physical baseline; the registry delta is comment-only.', 'Acceptance: discovery path/speed baseline, 58/58, analog 22, digital 36, report-backed 58, actual InformationReport, polling 0.', 'SCL target: CBClsCounter live runtime unchanged; saved/reopened SCL INS/INT32.' ) | Set-Content .\ARSAS\dist\SCL-EXPORT-ONLY-CANDIDATE.txt -Encoding utf8