From b5130f4ab2177ed9b00ee0dcc6c47ef1251ee304 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 20 Sep 2026 13:03:58 +0700 Subject: [PATCH 1/4] test: integrate typed SCL semantic engine candidate --- engines/ARIEC61850.lock.json | 6 +- evidence/p0.7-release-candidate-lock.json | 16 +- .../P07ReleaseCandidateLockRegressionTests.cs | 25 ++- .../SclSemanticIntegrationRegressionTests.cs | 158 ++++++++++++++++++ 4 files changed, 195 insertions(+), 10 deletions(-) create mode 100644 tests/ARSAS.Tests/SclSemanticIntegrationRegressionTests.cs diff --git a/engines/ARIEC61850.lock.json b/engines/ARIEC61850.lock.json index 360e72a96..e3fc11b11 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": "090d81944791be5b690d24342f9262495eda1a09", + "sourcePullRequest": 139, + "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. P0.7 semantic-integration candidate PR #139 is intentionally pinned on a stacked ARSAS branch only: exact live stVal TypeSpecification becomes status CDC authority and the SCL export boundary revalidates exact live status semantics before serialization. This candidate must preserve the locked physical P0.7 runtime/reporting baseline and remains physically unaccepted until relay retest.", "previousTrialPin": { "commit": "4467124775d8d9d76f3db194f9fbfd97144767a8", "sourcePullRequest": 134, diff --git a/evidence/p0.7-release-candidate-lock.json b/evidence/p0.7-release-candidate-lock.json index 9e44e5ef9..519ff8e53 100644 --- a/evidence/p0.7-release-candidate-lock.json +++ b/evidence/p0.7-release-candidate-lock.json @@ -75,7 +75,7 @@ } }, "sclSemanticParity": { - "status": "open-before-release", + "status": "integration-candidate-ci-passed-physical-retest-pending", "target": "Generated SCL must preserve the actual IED semantic model and converge with the trusted independent interoperability reference SCL for semantically authoritative CDC/type information without changing the accepted acquisition baseline.", "knownExample": { "object": "CBClsCounter", @@ -83,9 +83,19 @@ "expectedCdc": "INS", "expectedTypeFamily": "integer", "engineCandidatePullRequest": 139, - "engineCandidateHead": "9123c8aa1cc51a1e13c6750c0e1f8bee2a2de3b7", + "engineCandidateHead": "090d81944791be5b690d24342f9262495eda1a09", "integrationRule": "Any integration of this engine semantic correction must rerun and preserve every P0.7 accepted runtime and presentation invariant." + }, + "integrationCandidate": { + "repository": "masarray/ARIEC61850", + "pullRequest": 139, + "engineCommit": "090d81944791be5b690d24342f9262495eda1a09", + "ciRun": 691, + "ciStatus": "success", + "scope": "Exact live stVal TypeSpecification is status CDC authority through discovery, SCL export and SCL reopen. Exporter corrects stale conservative live CDC only when exact live typed evidence proves a stronger semantic result.", + "acquisitionChanges": false, + "physicalAcceptancePending": true } }, - "releaseRule": "Do not call the application release-ready while SCL semantic parity remains open. Continue SCL/CDC/type corrections from this locked baseline and reject any candidate that regresses the accepted runtime, report-only, Save-SCL, reuse, or presentation behavior." + "releaseRule": "Do not call the application release-ready until the SCL semantic integration candidate passes physical retest while preserving every locked P0.7 runtime, report-only, Save-SCL, reuse and presentation invariant." } diff --git a/tests/ARSAS.Tests/P07ReleaseCandidateLockRegressionTests.cs b/tests/ARSAS.Tests/P07ReleaseCandidateLockRegressionTests.cs index fc57090c3..d7d536d97 100644 --- a/tests/ARSAS.Tests/P07ReleaseCandidateLockRegressionTests.cs +++ b/tests/ARSAS.Tests/P07ReleaseCandidateLockRegressionTests.cs @@ -67,24 +67,41 @@ public void LockFile_FreezesThePhysicallyAcceptedRuntimeBaselineAndOpenSclSemant Assert.Equal("integer", known.GetProperty("expectedTypeFamily").GetString()); Assert.Equal(139, known.GetProperty("engineCandidatePullRequest").GetInt32()); Assert.Equal( - "9123c8aa1cc51a1e13c6750c0e1f8bee2a2de3b7", + "090d81944791be5b690d24342f9262495eda1a09", known.GetProperty("engineCandidateHead").GetString()); + var integration = scl.GetProperty("integrationCandidate"); + Assert.Equal(139, integration.GetProperty("pullRequest").GetInt32()); + Assert.Equal( + "090d81944791be5b690d24342f9262495eda1a09", + integration.GetProperty("engineCommit").GetString()); + Assert.Equal("success", integration.GetProperty("ciStatus").GetString()); + Assert.False(integration.GetProperty("acquisitionChanges").GetBoolean()); + Assert.True(integration.GetProperty("physicalAcceptancePending").GetBoolean()); } [Fact] - public void CurrentEngineLock_RemainsOnThePhysicalBaselineUntilSemanticCandidateIsExplicitlyIntegrated() + public void SemanticIntegrationBranch_PinsOnlyTheExplicitRecordedCandidate_WhileBaselineRemainsFrozen() { using var baseline = JsonDocument.Parse( File.ReadAllText(FindRepoFile("evidence/p0.7-release-candidate-lock.json"))); using var engine = JsonDocument.Parse( File.ReadAllText(FindRepoFile("engines/ARIEC61850.lock.json"))); - var expected = baseline.RootElement + var physicalBaseline = baseline.RootElement .GetProperty("physicalReference") .GetProperty("engineBaseline") .GetString(); + var candidate = baseline.RootElement + .GetProperty("sclSemanticParity") + .GetProperty("integrationCandidate") + .GetProperty("engineCommit") + .GetString(); - Assert.Equal(expected, engine.RootElement.GetProperty("commit").GetString()); + Assert.Equal( + "648124097621046f5f127ceb1cf853fea54db730", + physicalBaseline); + Assert.Equal(candidate, engine.RootElement.GetProperty("commit").GetString()); + Assert.NotEqual(physicalBaseline, candidate); } [Theory] diff --git a/tests/ARSAS.Tests/SclSemanticIntegrationRegressionTests.cs b/tests/ARSAS.Tests/SclSemanticIntegrationRegressionTests.cs new file mode 100644 index 000000000..6810faed8 --- /dev/null +++ b/tests/ARSAS.Tests/SclSemanticIntegrationRegressionTests.cs @@ -0,0 +1,158 @@ +using System.Xml.Linq; +using AR.Iec61850.Discovery; +using AR.Iec61850.Scl.Engineering; +using AR.Iec61850.Scl.Export; +using ArIED61850Tester.Models; + +namespace ARSAS.Tests; + +public sealed class SclSemanticIntegrationRegressionTests +{ + [Fact] + public void ExactIntegerCounter_ExportsAndReopensAsInsInt32_WithIntegerPresentation() + { + var model = new LiveIedModelDiscoveryDocument + { + 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.38, + ConfidenceLevel = LiveIedDiscoveryConfidenceLevel.Medium, + Attributes = + [ + Attribute("stVal", "INT32", "integer"), + Attribute("q", "Quality", "bit-string"), + Attribute("t", "Timestamp", "utc-time") + ] + } + ] + } + ] + } + ] + }; + + var document = LiveIedSclExporter.BuildDocument( + model, + new LiveIedSclExportOptions { Profile = "full-model" }); + + AssertExportedCounter(document); + + var reopened = SclLiveModelProjectionBuilder.Build( + document, + "generated.iid"); + var counter = Assert.Single( + reopened.LogicalDevices + .SelectMany(device => device.LogicalNodes) + .SelectMany(node => node.DataObjects), + dataObject => dataObject.Name == "CBClsCounter"); + + Assert.Equal("INS", counter.InferredCdc); + var stVal = Assert.Single( + counter.Attributes, + attribute => attribute.AttributePath == "stVal"); + Assert.Equal("INT32", stVal.SclBType); + Assert.Equal( + LiveIedDiscoveryConfidenceLevel.Exact, + stVal.TypeConfidence); + Assert.Equal("I", Iec61850ValueStatePresentation.TypeToken(stVal.SclBType)); + } + + [Fact] + public void PhysicalP07AcquisitionBaseline_RemainsFrozenDuringSemanticIntegration() + { + var runtime = File.ReadAllText( + FindRepoFile("Services/Iec61850MonitorRuntime.cs")); + var save = File.ReadAllText( + FindRepoFile("MainWindow.xaml.cs")); + var structured = File.ReadAllText( + FindRepoFile("tests/ARSAS.Tests/DiscoveryStructuredAnalogParityTests.cs")); + + Assert.Contains("cyclic MMS process polling=0", runtime, StringComparison.Ordinal); + Assert.Contains("state.NextPollUtc = DateTime.MaxValue", runtime, StringComparison.Ordinal); + Assert.Contains( + "monitoring remains active; Save SCL uses the current canonical model", + save, + StringComparison.Ordinal); + Assert.Contains( + "TwelveStructuredAnalogMembers_AllResolveToUniquePublishableFloatLeaves", + structured, + StringComparison.Ordinal); + } + + 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 = "LiveMmsTypeSpecification", + SclBType = sclBType, + MmsType = mmsType, + TypeDiscoveryStatus = "Exact", + TypeSource = "LiveMmsTypeSpecification", + TypeConfidence = LiveIedDiscoveryConfidenceLevel.Exact, + FunctionalConstraintConfidence = LiveIedDiscoveryConfidenceLevel.Exact + }; + + private static void AssertExportedCounter(XDocument document) + { + 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")); + var stVal = Assert.Single( + doType.Elements(ns + "DA"), + element => (string?)element.Attribute("name") == "stVal"); + Assert.Equal("INT32", (string?)stVal.Attribute("bType")); + } + + 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 0ffd4f36ae0bcef5233a8c1cc615531a3fd9ff3c Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 20 Sep 2026 13:06:16 +0700 Subject: [PATCH 2/4] ci: add isolated SCL semantic parity candidate gate --- .../scl-semantic-parity-candidate.yml | 225 ++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 .github/workflows/scl-semantic-parity-candidate.yml diff --git a/.github/workflows/scl-semantic-parity-candidate.yml b/.github/workflows/scl-semantic-parity-candidate.yml new file mode 100644 index 000000000..d9e826e5b --- /dev/null +++ b/.github/workflows/scl-semantic-parity-candidate.yml @@ -0,0 +1,225 @@ +name: SCL Semantic Parity Candidate + +on: + pull_request: + paths: + - "engines/ARIEC61850.lock.json" + - "evidence/p0.7-release-candidate-lock.json" + - "tests/ARSAS.Tests/P07ReleaseCandidateLockRegressionTests.cs" + - "tests/ARSAS.Tests/SclSemanticIntegrationRegressionTests.cs" + - ".github/workflows/scl-semantic-parity-candidate.yml" + workflow_dispatch: + +concurrency: + group: scl-semantic-parity-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + build-semantic-candidate: + name: Build locked SCL semantic integration candidate + runs-on: windows-latest + + steps: + - name: Checkout exact ARSAS semantic candidate + shell: pwsh + env: + ARSAS_SOURCE_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: | + if ($env:ARSAS_SOURCE_SHA -notmatch '^[0-9a-f]{40}$') { + throw "Invalid ARSAS source SHA: $env:ARSAS_SOURCE_SHA" + } + + 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. Expected $env:ARSAS_SOURCE_SHA, got $actual." + } + + "ARSAS_COMMIT=$actual" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append + + - name: Validate frozen P0.7 baseline and explicit semantic engine candidate + shell: pwsh + run: | + $lock = Get-Content .\ARSAS\engines\ARIEC61850.lock.json -Raw | ConvertFrom-Json + $baseline = Get-Content .\ARSAS\evidence\p0.7-release-candidate-lock.json -Raw | ConvertFrom-Json + + if ($lock.repository -ne 'masarray/ARIEC61850' -or + $lock.commit -notmatch '^[0-9a-f]{40}$' -or + [int]$lock.sourcePullRequest -ne 139) { + throw 'Semantic candidate must pin the explicit ARIEC61850 PR #139 revision.' + } + + if ($baseline.contractId -ne 'P0.7-RELEASE-CANDIDATE-LOCK') { + throw 'P0.7 release-candidate lock is missing or changed.' + } + + if ($baseline.physicalReference.consumerAcceptedHead -ne '0d0b9204d6637e3d62e2eee94000386ae43cd0e9' -or + $baseline.physicalReference.engineBaseline -ne '648124097621046f5f127ceb1cf853fea54db730') { + throw 'Physically accepted P0.7 source authority was modified.' + } + + $runtime = $baseline.acceptedRuntime + if ([int]$runtime.staticMembers -ne 58 -or + [int]$runtime.selectedLiveRows -ne 58 -or + [int]$runtime.analogRows -ne 22 -or + [int]$runtime.digitalRows -ne 36 -or + [int]$runtime.reportBackedRows -ne 58 -or + [int]$runtime.cyclicMmsProcessPolling -ne 0 -or + -not [bool]$runtime.actualInformationReportObserved -or + -not [bool]$runtime.saveSclWhileMonitoring -or + -not [bool]$runtime.savedSclReusable -or + [int]$runtime.reopenedSclLiveRows -ne 58) { + throw 'P0.7 physical runtime acceptance was weakened.' + } + + $candidate = $baseline.sclSemanticParity.integrationCandidate + if ($candidate.repository -ne 'masarray/ARIEC61850' -or + [int]$candidate.pullRequest -ne 139 -or + $candidate.engineCommit -ne $lock.commit -or + [bool]$candidate.acquisitionChanges -or + -not [bool]$candidate.physicalAcceptancePending) { + throw 'SCL semantic integration evidence does not match the engine lock.' + } + + if ($baseline.sclSemanticParity.knownExample.object -ne 'CBClsCounter' -or + $baseline.sclSemanticParity.knownExample.expectedCdc -ne 'INS' -or + $baseline.sclSemanticParity.knownExample.expectedTypeFamily -ne 'integer') { + throw 'Known integer-status semantic acceptance was weakened.' + } + + "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 semantic 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().ToLowerInvariant() + if ($actual -ne $env:ARIEC61850_COMMIT.ToLowerInvariant()) { + throw "Engine SHA mismatch. Expected $env:ARIEC61850_COMMIT, got $actual." + } + + - name: Verify semantic export boundary and acquisition freeze + shell: pwsh + run: | + $cdc = Get-Content .\ARIEC61850\src\AR.Iec61850\Discovery\CdcInferenceEngine.cs -Raw + $exporter = Get-Content .\ARIEC61850\src\AR.Iec61850\Scl\Export\LiveIedSclExporter.cs -Raw + $engineRegression = Get-Content .\ARIEC61850\tests\AR.Iec61850.Tests\Scl\LiveIedSclSemanticAuthorityTests.cs -Raw + $runtime = Get-Content .\ARSAS\Services\Iec61850MonitorRuntime.cs -Raw + $save = Get-Content .\ARSAS\MainWindow.xaml.cs -Raw + $consumerRegression = Get-Content .\ARSAS\tests\ARSAS.Tests\SclSemanticIntegrationRegressionTests.cs -Raw + + foreach ($required in @( + 'TryInferTypedStatusCdc', + 'exact stVal type is', + 'ResolveExportCdc', + 'ExactLiveTypeCdcAuthority')) { + if (($cdc + $exporter) -notmatch [regex]::Escape($required)) { + throw "Typed SCL semantic authority is incomplete: $required" + } + } + + foreach ($required in @( + 'ExactLiveIntegerStatus_RepairsStaleBooleanCdc_AndRoundTripsAsInsInt32', + 'Assert.Equal("INS"', + 'Assert.Equal("INT32"')) { + if ($engineRegression -notmatch [regex]::Escape($required)) { + throw "Engine SCL round-trip regression is incomplete: $required" + } + } + + if ($runtime -notmatch 'cyclic MMS process polling=0' -or + $runtime -notmatch 'state\.NextPollUtc = DateTime\.MaxValue' -or + $save -notmatch 'monitoring remains active; Save SCL uses the current canonical model' -or + $consumerRegression -notmatch 'ExactIntegerCounter_ExportsAndReopensAsInsInt32_WithIntegerPresentation') { + throw 'P0.7 acquisition freeze or consumer semantic regression contract is missing.' + } + + - name: Setup .NET 8 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Restore engine + run: dotnet restore .\ARIEC61850\ARIEC61850.sln + + - name: Build engine + run: dotnet build .\ARIEC61850\ARIEC61850.sln -c Release --no-restore + + - name: Test engine + run: dotnet test .\ARIEC61850\ARIEC61850.sln -c Release --no-build --no-restore --logger "trx;LogFileName=ariec61850-scl-semantic-tests.trx" --results-directory .\ARSAS\TestResults + + - name: Restore ARSAS + run: dotnet restore .\ARSAS\ArIED61850Tester.sln + + - name: Build ARSAS + run: dotnet build .\ARSAS\ArIED61850Tester.sln -c Release --no-restore + + - name: Run full ARSAS regression suite + run: dotnet test .\ARSAS\tests\ARSAS.Tests\ARSAS.Tests.csproj -c Release --no-build --no-restore --logger "trx;LogFileName=arsas-scl-semantic-tests.trx" --results-directory .\ARSAS\TestResults + + - name: Publish semantic candidate portable 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 semantic candidate + 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-scl-semantic-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 ($process.ExitCode -ne 0) { + throw "Portable EXE smoke test failed: $($process.ExitCode)" + } + + $baseline = Get-Content .\ARSAS\evidence\p0.7-release-candidate-lock.json -Raw | ConvertFrom-Json + @( + 'ARSAS SCL semantic parity physical-retest candidate', + "ARSAS commit: $env:ARSAS_COMMIT", + "ARIEC61850 commit: $env:ARIEC61850_COMMIT", + "Accepted physical engine baseline: $($baseline.physicalReference.engineBaseline)", + '', + 'Candidate semantic acceptance:', + 'CBClsCounter exact integer stVal -> SCL CDC INS -> stVal INT32 -> reopened integer presentation.', + '', + 'Frozen physical acceptance to preserve:', + 'Static members/live rows 58/58; Analog 22/22; Digital 36/36; report-backed 58; cyclic MMS process polling 0.', + 'Save SCL while monitoring must remain non-disruptive and generated SCL must reopen into the same report-backed workflow.', + '', + 'This artifact is a physical-retest candidate, not release authority.' + ) | Set-Content .\ARSAS\dist\SCL-SEMANTIC-CANDIDATE.txt -Encoding utf8 + + - name: Upload semantic candidate + uses: actions/upload-artifact@v4 + with: + name: ARSAS-scl-semantic-parity-win-x64 + path: | + ARSAS\dist\ARSAS-*-win-x64-portable.exe + ARSAS\dist\SCL-SEMANTIC-CANDIDATE.txt + ARSAS\evidence\p0.7-release-candidate-lock.json + ARSAS\TestResults\*.trx + if-no-files-found: error + retention-days: 14 From 97a7e3ea330ccf7e5b15827e88d6ef96e12f76e2 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 20 Sep 2026 13:09:32 +0700 Subject: [PATCH 3/4] tests: recognize explicit semantic integration candidate --- 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 d7d536d97..208678d02 100644 --- a/tests/ARSAS.Tests/P07ReleaseCandidateLockRegressionTests.cs +++ b/tests/ARSAS.Tests/P07ReleaseCandidateLockRegressionTests.cs @@ -60,7 +60,7 @@ public void LockFile_FreezesThePhysicallyAcceptedRuntimeBaselineAndOpenSclSemant Assert.Contains("ThdPPV.phsBC", structured); var scl = root.GetProperty("sclSemanticParity"); - Assert.Equal("open-before-release", scl.GetProperty("status").GetString()); + Assert.Equal("integration-candidate-ci-passed-physical-retest-pending", scl.GetProperty("status").GetString()); var known = scl.GetProperty("knownExample"); Assert.Equal("CBClsCounter", known.GetProperty("object").GetString()); Assert.Equal("INS", known.GetProperty("expectedCdc").GetString()); From 32dc8542914d069e8fa49fe6ea6574fdaf950564 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 20 Sep 2026 13:09:35 +0700 Subject: [PATCH 4/4] tests: preserve physical engine authority under semantic pin --- .../CanonicalLiveSclExportRegressionTests.cs | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs b/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs index 67364653b..ae47a62e7 100644 --- a/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs +++ b/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs @@ -396,19 +396,37 @@ public void SourceClean_GuardsApprovedFirstPartyConvergenceAuthorities() [Fact] - public void EnginePin_MatchesPhysicalSclRepairHead() + public void EnginePin_PreservesPhysicalSclRepairAuthorityWhileUsingExplicitSemanticCandidate() { var lockFile = File.ReadAllText(FindRepoFile("engines/ARIEC61850.lock.json")); + var semanticEvidence = File.ReadAllText( + FindRepoFile("evidence/p0.7-release-candidate-lock.json")); Assert.Contains( - "\"commit\": \"648124097621046f5f127ceb1cf853fea54db730\"", + "\"commit\": \"090d81944791be5b690d24342f9262495eda1a09\"", lockFile, StringComparison.Ordinal); + Assert.Contains("\"sourcePullRequest\": 139", lockFile, StringComparison.Ordinal); + + // The physical R7/R10 authority remains immutable provenance and is not + // relabeled as physically accepted merely because a semantic candidate is pinned. Assert.Contains( "\"physicalTestedCommit\": \"9935d6902d786cc69b299260fe36b835944d5e81\"", lockFile, StringComparison.Ordinal); - Assert.Contains("\"sourcePullRequest\": 135", lockFile, StringComparison.Ordinal); + Assert.Contains( + "\"mergedMainCommit\": \"648124097621046f5f127ceb1cf853fea54db730\"", + lockFile, + StringComparison.Ordinal); + Assert.Contains( + "\"engineBaseline\": \"648124097621046f5f127ceb1cf853fea54db730\"", + semanticEvidence, + StringComparison.Ordinal); + Assert.Contains( + "\"physicalAcceptancePending\": true", + semanticEvidence, + 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); @@ -425,6 +443,7 @@ public void EnginePin_MatchesPhysicalSclRepairHead() 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("P0.7 semantic-integration candidate PR #139", lockFile, StringComparison.Ordinal); Assert.Contains( "\"commit\": \"4467124775d8d9d76f3db194f9fbfd97144767a8\"", lockFile,