From 0c806ad8159b31758766214a73aaf1c5c87abd79 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 20 Sep 2026 16:55:09 +0700 Subject: [PATCH 01/13] fix: track physical-proven smart discovery route in production source --- Services/NativeIec61850Client.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Services/NativeIec61850Client.cs b/Services/NativeIec61850Client.cs index a78d2c19f..0bf4a9d7c 100644 --- a/Services/NativeIec61850Client.cs +++ b/Services/NativeIec61850Client.cs @@ -62,6 +62,7 @@ public async Task ConnectAsync(string ipAddress, int port, CancellationToken can LastConnectionTechnicalSummary = string.Empty; _lastDiscovery = null; _liveModel = null; + ResetSmartDiscoveryAuthority(); // __P0_5C_CONNECT_RESET__ ClearCanonicalModel(); _reportMonitorSessions.Clear(); _reportMonitorCoverage.Clear(); @@ -127,6 +128,11 @@ public Iec61850DeviceDiagnosticSnapshot CaptureDiagnosticSnapshot( public async Task> DiscoverSignalsAsync(CancellationToken cancellationToken, IProgress? progress = null) { + // Production authority: this is the exact physical-proven R7 smart discovery + // route. Release/installer/local builds must execute the same path. + if (SmartDiscoveryCaptureModeEnabled) + return await DiscoverSignalsSmartForCaptureAsync(cancellationToken, progress).ConfigureAwait(false); + LastDiscoverySummary = string.Empty; cancellationToken.ThrowIfCancellationRequested(); progress?.Report(new IedDiscoveryProgress( @@ -1950,6 +1956,8 @@ private static Iec61850ControlCommandResult ControlWireUnknownFailure( public async ValueTask DisposeAsync() { + ResetSmartDiscoveryAuthority(); // __P0_5C_DISPOSE_RESET__ + await DisposeControlSessionsAsync().ConfigureAwait(false); await StopReportMonitorsAsync().ConfigureAwait(false); await _mmsIoGate.WaitAsync().ConfigureAwait(false); @@ -1979,7 +1987,9 @@ public async ValueTask DisposeAsync() var service = new ArControl.Iec61850ControlService(); var opened = await RunMmsOperationAsync( - () => service.OpenAsync(_session, signal.ObjectReference, cancellationToken), + () => _lastDiscovery != null + ? service.OpenAsync(_session, signal.ObjectReference, _lastDiscovery.Snapshot.DomainVariables, cancellationToken) + : service.OpenAsync(_session, signal.ObjectReference, cancellationToken), cancellationToken).ConfigureAwait(false); _controlSessions[key] = opened; return opened; From f36f44580acb71b161ba879e1f5db04314d03675 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 20 Sep 2026 16:55:22 +0700 Subject: [PATCH 02/13] build: make smart discovery route verification fail closed --- scripts/enable-smart-discovery-capture.ps1 | 113 +++++---------------- 1 file changed, 24 insertions(+), 89 deletions(-) diff --git a/scripts/enable-smart-discovery-capture.ps1 b/scripts/enable-smart-discovery-capture.ps1 index e25090bf4..a0eaa98dc 100644 --- a/scripts/enable-smart-discovery-capture.ps1 +++ b/scripts/enable-smart-discovery-capture.ps1 @@ -1,101 +1,36 @@ +param( + [switch]$VerifyOnly +) + $ErrorActionPreference = 'Stop' $sourcePath = Join-Path $PSScriptRoot '..\Services\NativeIec61850Client.cs' $sourcePath = [System.IO.Path]::GetFullPath($sourcePath) $text = [System.IO.File]::ReadAllText($sourcePath) -$changed = $false - -$routeMarker = 'DiscoverSignalsSmartForCaptureAsync(cancellationToken, progress)' -if ($text.IndexOf($routeMarker, [System.StringComparison]::Ordinal) -lt 0) { - $pattern = '(public async Task> DiscoverSignalsAsync\(CancellationToken cancellationToken, IProgress\? progress = null\)\s*\{)' - $match = [regex]::Match($text, $pattern) - if (-not $match.Success) { - throw 'Could not locate NativeIec61850Client.DiscoverSignalsAsync entrypoint.' - } - if ([regex]::Matches($text, $pattern).Count -ne 1) { - throw 'DiscoverSignalsAsync entrypoint is not unique; refusing ambiguous build-time patch.' - } - $injection = @' +$required = @( + 'if (SmartDiscoveryCaptureModeEnabled)', + 'return await DiscoverSignalsSmartForCaptureAsync(cancellationToken, progress).ConfigureAwait(false);', + '__P0_5C_CONNECT_RESET__', + '__P0_5C_DISPOSE_RESET__', + '_lastDiscovery.Snapshot.DomainVariables' +) - if (SmartDiscoveryCaptureModeEnabled) - return await DiscoverSignalsSmartForCaptureAsync(cancellationToken, progress).ConfigureAwait(false); -'@ +$missing = @($required | Where-Object { + $text.IndexOf($_, [System.StringComparison]::Ordinal) -lt 0 +}) - $text = $text.Insert($match.Index + $match.Length, $injection) - $changed = $true - Write-Host 'Installed PR #134 smart discovery capture route into NativeIec61850Client.DiscoverSignalsAsync.' +if ($missing.Count -eq 0) { + Write-Host 'Production smart discovery route verification passed: tracked source matches the physical-proven R7 route contract.' + exit 0 } -else { - Write-Host 'Smart discovery capture route already installed.' -} - -$resetMarker = '_liveModel = null;__P0_5C_CONNECT_RESET__' -if ($text.IndexOf('__P0_5C_CONNECT_RESET__', [System.StringComparison]::Ordinal) -lt 0) { - $resetAnchor = " _lastDiscovery = null;`r`n _liveModel = null;" - $anchorIndex = $text.IndexOf($resetAnchor, [System.StringComparison]::Ordinal) - if ($anchorIndex -lt 0) { - $resetAnchor = " _lastDiscovery = null;`n _liveModel = null;" - $anchorIndex = $text.IndexOf($resetAnchor, [System.StringComparison]::Ordinal) - } - if ($anchorIndex -lt 0) { - throw 'Could not locate ConnectAsync discovery reset anchor for smart authority invalidation.' - } - if ($text.IndexOf($resetAnchor, $anchorIndex + $resetAnchor.Length, [System.StringComparison]::Ordinal) -ge 0) { - throw 'ConnectAsync discovery reset anchor is not unique; refusing ambiguous smart authority patch.' - } - $lineBreak = if ($resetAnchor.Contains("`r`n")) { "`r`n" } else { "`n" } - $resetInjection = $resetAnchor + $lineBreak + ' ResetSmartDiscoveryAuthority(); // __P0_5C_CONNECT_RESET__' - $text = $text.Remove($anchorIndex, $resetAnchor.Length).Insert($anchorIndex, $resetInjection) - $changed = $true - Write-Host 'Installed P0-5c association-generation reset into ConnectAsync.' +if ($VerifyOnly) { + throw "Production smart discovery route is incomplete. Missing tracked contract marker(s): $($missing -join '; ')" } -else { - Write-Host 'P0-5c ConnectAsync association reset already installed.' -} - -$disposeMarker = '__P0_5C_DISPOSE_RESET__' -if ($text.IndexOf($disposeMarker, [System.StringComparison]::Ordinal) -lt 0) { - $disposePattern = '(public async ValueTask DisposeAsync\(\)\s*\{)' - $disposeMatch = [regex]::Match($text, $disposePattern) - if (-not $disposeMatch.Success -or [regex]::Matches($text, $disposePattern).Count -ne 1) { - throw 'Could not locate a unique NativeIec61850Client.DisposeAsync entrypoint for association invalidation.' - } - $disposeInjection = @' - - ResetSmartDiscoveryAuthority(); // __P0_5C_DISPOSE_RESET__ -'@ - $text = $text.Insert($disposeMatch.Index + $disposeMatch.Length, $disposeInjection) - $changed = $true - Write-Host 'Installed P0-5c association-generation reset into DisposeAsync.' -} -else { - Write-Host 'P0-5c DisposeAsync association reset already installed.' -} - -$controlAuthorityMarker = '_lastDiscovery.Snapshot.DomainVariables' -if ($text.IndexOf($controlAuthorityMarker, [System.StringComparison]::Ordinal) -lt 0) { - $controlPattern = '\(\) => service\.OpenAsync\(_session, signal\.ObjectReference, cancellationToken\)' - $controlMatches = [regex]::Matches($text, $controlPattern) - if ($controlMatches.Count -ne 1) { - throw "Expected exactly one control OpenAsync discovery call, found $($controlMatches.Count); refusing ambiguous authority patch." - } - - $controlReplacement = @' -() => _lastDiscovery != null - ? service.OpenAsync(_session, signal.ObjectReference, _lastDiscovery.Snapshot.DomainVariables, cancellationToken) - : service.OpenAsync(_session, signal.ObjectReference, cancellationToken) -'@ - $text = [regex]::Replace($text, $controlPattern, $controlReplacement, 1) - $changed = $true - Write-Host 'Installed authoritative smart domain inventory reuse into control inspection.' -} -else { - Write-Host 'Control inspection already reuses authoritative smart domain inventory.' -} - -if ($changed) { - [System.IO.File]::WriteAllText($sourcePath, $text, (New-Object System.Text.UTF8Encoding($false))) -} +throw @" +The physical-proven Smart Discovery route is no longer permitted to be inserted only at build time. +Repair Services\NativeIec61850Client.cs in tracked source and commit the exact R7 route before building. +Missing marker(s): $($missing -join '; ') +"@ From 9a0d4b9a710431a3fb33a2a2843962688e12a07b Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 20 Sep 2026 16:55:26 +0700 Subject: [PATCH 03/13] build: remove workflow-dependent discovery compilation --- Directory.Build.targets | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/Directory.Build.targets b/Directory.Build.targets index 2db26be69..676481672 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,22 +1,9 @@ - - - - - true - - - true - false - - - - - + Condition="'$(MSBuildProjectName)' == 'ArIED61850Tester'"> + + From fb57d72b04a73ecd9f10d518735d70abb02887f9 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 20 Sep 2026 16:55:39 +0700 Subject: [PATCH 04/13] test: lock production packaging to physical-proven smart route --- ...overyProductionPromotionRegressionTests.cs | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/tests/ARSAS.Tests/SmartDiscoveryProductionPromotionRegressionTests.cs b/tests/ARSAS.Tests/SmartDiscoveryProductionPromotionRegressionTests.cs index 7c8494f50..c91bb777c 100644 --- a/tests/ARSAS.Tests/SmartDiscoveryProductionPromotionRegressionTests.cs +++ b/tests/ARSAS.Tests/SmartDiscoveryProductionPromotionRegressionTests.cs @@ -35,20 +35,24 @@ public void P05g_TargetKeepsProductionPromotionFailClosed() } [Fact] - public void P05g_DefaultBuildDoesNotPromoteFieldRoute() + public void ProductionBuildTracksPhysicalProvenSmartRouteForEveryPackagingLane() { - var props = XDocument.Load(FindRepoFile("evidence/SmartDiscoveryPromotion.props")); - var promoted = props.Descendants("SmartDiscoveryProductionPromoted").Single().Value.Trim(); - Assert.Equal("false", promoted, ignoreCase: true); - Assert.Empty(props.Descendants("SmartDiscoveryPromotionAuthoritySha256")); - Assert.Empty(props.Descendants("SmartDiscoveryValidatedEngineHead")); + var native = File.ReadAllText(FindRepoFile("Services/NativeIec61850Client.cs")); + Assert.Contains("if (SmartDiscoveryCaptureModeEnabled)", native, StringComparison.Ordinal); + Assert.Contains("return await DiscoverSignalsSmartForCaptureAsync(cancellationToken, progress).ConfigureAwait(false);", native, StringComparison.Ordinal); + Assert.Contains("__P0_5C_CONNECT_RESET__", native, StringComparison.Ordinal); + Assert.Contains("__P0_5C_DISPOSE_RESET__", native, StringComparison.Ordinal); + Assert.Contains("_lastDiscovery.Snapshot.DomainVariables", native, StringComparison.Ordinal); var targets = File.ReadAllText(FindRepoFile("Directory.Build.targets")); - Assert.Contains("GITHUB_WORKFLOW", targets, StringComparison.Ordinal); - Assert.Contains("Smart Discovery Field Capture Build", targets, StringComparison.Ordinal); - Assert.Contains("SmartDiscoveryProductionPromoted", targets, StringComparison.Ordinal); - Assert.Contains("EnableSmartDiscoveryCaptureRoute", targets, StringComparison.Ordinal); - Assert.Contains(">false", targets, StringComparison.Ordinal); + Assert.DoesNotContain("GITHUB_WORKFLOW", targets, StringComparison.Ordinal); + Assert.DoesNotContain("SmartDiscoveryProductionPromoted", targets, StringComparison.Ordinal); + Assert.Contains("VerifyPhysicalProvenSmartDiscoveryRoute", targets, StringComparison.Ordinal); + Assert.Contains("-VerifyOnly", targets, StringComparison.Ordinal); + + var verifier = File.ReadAllText(FindRepoFile("scripts/enable-smart-discovery-capture.ps1")); + Assert.Contains("Production smart discovery route verification passed", verifier, StringComparison.Ordinal); + Assert.Contains("no longer permitted to be inserted only at build time", verifier, StringComparison.Ordinal); } [Fact] From 90fc87d7295101b4211b507ce1e101934d3133f2 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 20 Sep 2026 16:56:04 +0700 Subject: [PATCH 05/13] ci: enforce one smart discovery route for production builds --- .../smart-discovery-production-promotion.yml | 183 +++++------------- 1 file changed, 53 insertions(+), 130 deletions(-) diff --git a/.github/workflows/smart-discovery-production-promotion.yml b/.github/workflows/smart-discovery-production-promotion.yml index f649aef4d..98be3c3e4 100644 --- a/.github/workflows/smart-discovery-production-promotion.yml +++ b/.github/workflows/smart-discovery-production-promotion.yml @@ -5,162 +5,85 @@ on: workflow_dispatch: jobs: - verify-production-promotion: - name: Verify P0-5g production promotion and merge-readiness contract + verify-production-route: + name: Verify physical-proven Smart Discovery production parity runs-on: windows-latest + timeout-minutes: 30 + steps: - - name: Checkout ARSAS branch + - name: Checkout exact ARSAS candidate + uses: actions/checkout@v4 + with: + path: ArIED61850Tester + + - name: Resolve immutable ARIEC61850 engine lock shell: powershell run: | - $ref = if ($env:GITHUB_HEAD_REF) { $env:GITHUB_HEAD_REF } else { $env:GITHUB_REF_NAME } - git clone --quiet --branch $ref "https://github.com/$env:GITHUB_REPOSITORY.git" ArIED61850Tester - $arsasHead = (git -C .\ArIED61850Tester rev-parse HEAD).Trim().ToLowerInvariant() - "ARSAS_HEAD=$arsasHead" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append - Write-Host "P0-5g ARSAS head: $arsasHead" + $lock = Get-Content '.\ArIED61850Tester\engines\ARIEC61850.lock.json' -Raw | ConvertFrom-Json + if ($lock.repository -ne 'masarray/ARIEC61850' -or $lock.commit -notmatch '^[0-9a-f]{40}$') { + throw 'Invalid ARIEC61850 production lock.' + } + "ARIEC61850_REPOSITORY=$($lock.repository)" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append + "ARIEC61850_COMMIT=$($lock.commit)" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append - - name: Checkout ARIEC61850 PR 134 head + - name: Checkout exact ARIEC61850 engine shell: powershell run: | - git clone --quiet --branch perf/smart-ied-discovery https://github.com/masarray/ARIEC61850.git ARIEC61850 - $engineHead = (git -C .\ARIEC61850 rev-parse HEAD).Trim().ToLowerInvariant() - "ENGINE_HEAD=$engineHead" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append - Write-Host "P0-5g engine PR head: $engineHead" + 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 pin mismatch: expected $env:ARIEC61850_COMMIT got $actual" + } - name: Setup .NET 8 uses: actions/setup-dotnet@v4 with: dotnet-version: 8.0.x - - name: Validate P0-5g source contract + - name: Verify tracked production route before build shell: powershell run: | - $root = '.\ArIED61850Tester' - $required = @( - "$root\evidence\SmartDiscoveryPromotion.props", - "$root\evidence\smart-discovery-production-promotion-target.json", - "$root\scripts\verify-smart-discovery-production-readiness.ps1", - "$root\scripts\new-smart-discovery-production-promotion-authority.ps1", - "$root\docs\P0-5G_PRODUCTION_PROMOTION_MAINLINE_READINESS.md", - "$root\tests\ARSAS.Tests\SmartDiscoveryProductionPromotionRegressionTests.cs" - ) - foreach ($path in $required) { - if (-not (Test-Path $path -PathType Leaf)) { throw "P0-5g source missing: $path" } - } - foreach ($script in @( - "$root\scripts\verify-smart-discovery-production-readiness.ps1", - "$root\scripts\new-smart-discovery-production-promotion-authority.ps1")) { - $tokens = $null - $errors = $null - [System.Management.Automation.Language.Parser]::ParseFile($script, [ref]$tokens, [ref]$errors) | Out-Null - if ($errors.Count -ne 0) { - $messages = @($errors | ForEach-Object { $_.Message }) -join '; ' - throw "PowerShell parse failure in ${script}: $messages" - } + $native = '.\ArIED61850Tester\Services\NativeIec61850Client.cs' + $before = (Get-FileHash $native -Algorithm SHA256).Hash + & .\ArIED61850Tester\scripts\enable-smart-discovery-capture.ps1 -VerifyOnly + if ($LASTEXITCODE -ne 0) { throw 'Smart Discovery route verification failed.' } + $after = (Get-FileHash $native -Algorithm SHA256).Hash + if ($after -ne $before) { + throw 'Verification mutated NativeIec61850Client.cs; build-time source mutation is forbidden.' } - [xml]$props = Get-Content "$root\evidence\SmartDiscoveryPromotion.props" -Raw - $promoted = ([string]$props.Project.PropertyGroup.SmartDiscoveryProductionPromoted).Trim().ToLowerInvariant() - if ($promoted -notin @('true','false')) { throw 'P0-5g production switch is not a boolean.' } - - - name: Build and test exact engine PR head - shell: powershell - run: | - .\ARIEC61850\scripts\verify-source-clean.ps1 - dotnet restore .\ARIEC61850\ARIEC61850.sln - dotnet build .\ARIEC61850\ARIEC61850.sln -c Release --no-restore - dotnet test .\ARIEC61850\tests\AR.Iec61850.Tests\AR.Iec61850.Tests.csproj -c Release --no-build --no-restore - - - name: Verify P0-5g readiness state - shell: powershell - run: | - $root = '.\ArIED61850Tester' - $verifier = "$root\scripts\verify-smart-discovery-production-readiness.ps1" - $physical = "$root\evidence\smart-discovery-repeat-run.authority.json" - $promotion = "$root\evidence\smart-discovery-production-promotion-authority.json" - $physicalArg = if (Test-Path $physical -PathType Leaf) { $physical } else { '' } - $promotionArg = if (Test-Path $promotion -PathType Leaf) { $promotion } else { '' } - $output = "$root\TestResults\P0-5G-production-readiness.json" - New-Item -ItemType Directory -Force "$root\TestResults" | Out-Null - - & $verifier ` - -TargetPath "$root\evidence\smart-discovery-production-promotion-target.json" ` - -EngineLockPath "$root\engines\ARIEC61850.lock.json" ` - -PromotionPropsPath "$root\evidence\SmartDiscoveryPromotion.props" ` - -ArsasRepositoryPath $root ` - -EngineRepositoryPath '.\ARIEC61850' ` - -ArsasHeadCommit $env:ARSAS_HEAD ` - -EngineHeadCommit $env:ENGINE_HEAD ` - -EngineHeadCiConclusion success ` - -PhysicalAuthorityPath $physicalArg ` - -PromotionAuthorityPath $promotionArg ` - -OutputJson $output ` - -NoFailExit - $result = Get-Content $output -Raw | ConvertFrom-Json - if (-not (Test-Path $physical -PathType Leaf)) { - if ($result.Verdict -ne 'BLOCKED' -or - -not (@($result.Blockers) -match 'P0-5f physical-finalized authority is missing')) { - throw 'P0-5g must remain fail-closed until physical P0-5f authority is present.' + $source = Get-Content $native -Raw + foreach ($required in @( + 'return await DiscoverSignalsSmartForCaptureAsync(cancellationToken, progress).ConfigureAwait(false);', + '__P0_5C_CONNECT_RESET__', + '__P0_5C_DISPOSE_RESET__', + '_lastDiscovery.Snapshot.DomainVariables')) { + if ($source -notmatch [regex]::Escape($required)) { + throw "Production Smart Discovery contract missing: $required" } - if ([bool]$result.ProductionSwitchEnabled) { - throw 'P0-5g production switch became enabled without physical authority.' - } - } elseif (-not (Test-Path $promotion -PathType Leaf)) { - if ($result.Verdict -ne 'READY_TO_PROMOTE') { - throw "Physical authority exists but P0-5g is not READY_TO_PROMOTE: $(@($result.Blockers) -join '; ')" - } - } elseif ($result.Verdict -ne 'READY_FOR_REVIEW') { - throw "Promotion authority exists but P0-5g is not READY_FOR_REVIEW: $(@($result.Blockers) -join '; ')" } - - name: Build and test default fail-closed ARSAS path + - name: Restore build and test the exact production route shell: powershell run: | - # The current engine PR head is validated above, but ordinary pre-promotion - # ARSAS remains pinned to the physical-evidence engine baseline. Build the - # fail-closed route against that exact lock rather than an arbitrary newer head. - $lock = Get-Content '.\ArIED61850Tester\engines\ARIEC61850.lock.json' -Raw | ConvertFrom-Json - git -C .\ARIEC61850 checkout --quiet --detach $lock.commit - $actualEngine = (git -C .\ARIEC61850 rev-parse HEAD).Trim().ToLowerInvariant() - if ($actualEngine -ne ([string]$lock.commit).ToLowerInvariant()) { - throw "P0-5g fail-closed engine checkout mismatch: $actualEngine" - } - - $native = '.\ArIED61850Tester\Services\NativeIec61850Client.cs' - $before = Get-Content $native -Raw - if ($before -match 'return await DiscoverSignalsSmartForCaptureAsync\(cancellationToken, progress\)') { - throw 'Tracked NativeIec61850Client.cs already contains the field-route patch before default build.' - } dotnet restore .\ArIED61850Tester\ArIED61850Tester.sln + if ($LASTEXITCODE -ne 0) { throw 'ARSAS restore failed.' } dotnet build .\ArIED61850Tester\ArIED61850Tester.sln -c Release --no-restore + if ($LASTEXITCODE -ne 0) { throw 'ARSAS production-route build failed.' } dotnet test .\ArIED61850Tester\tests\ARSAS.Tests\ARSAS.Tests.csproj -c Release --no-build --no-restore - $after = Get-Content $native -Raw - [xml]$props = Get-Content '.\ArIED61850Tester\evidence\SmartDiscoveryPromotion.props' -Raw - $promoted = ([string]$props.Project.PropertyGroup.SmartDiscoveryProductionPromoted).Trim().ToLowerInvariant() -eq 'true' - if (-not $promoted -and $after -match 'return await DiscoverSignalsSmartForCaptureAsync\(cancellationToken, progress\)') { - throw 'Default pre-promotion build unexpectedly installed the smart discovery route.' - } + if ($LASTEXITCODE -ne 0) { throw 'ARSAS production-route regression tests failed.' } - - name: Build and test explicit smart-route candidate + - name: Prove build did not select or synthesize another route shell: powershell run: | - dotnet build .\ArIED61850Tester\ArIED61850Tester.sln -c Release --no-restore -p:EnableSmartDiscoveryCaptureRoute=true - $native = Get-Content '.\ArIED61850Tester\Services\NativeIec61850Client.cs' -Raw - if ($native -notmatch 'return await DiscoverSignalsSmartForCaptureAsync\(cancellationToken, progress\)') { - throw 'Explicit P0-5g smart-route build did not install the discovery route.' - } - if ($native -notmatch '__P0_5C_CONNECT_RESET__' -or $native -notmatch '__P0_5C_DISPOSE_RESET__') { - throw 'Explicit P0-5g smart-route build did not install association lifecycle resets.' - } - if ($native -notmatch '_lastDiscovery\.Snapshot\.DomainVariables') { - throw 'Explicit P0-5g smart-route build did not install authoritative Control inventory reuse.' - } - dotnet test .\ArIED61850Tester\tests\ARSAS.Tests\ARSAS.Tests.csproj -c Release --no-build --no-restore + $native = '.\ArIED61850Tester\Services\NativeIec61850Client.cs' + & .\ArIED61850Tester\scripts\enable-smart-discovery-capture.ps1 -VerifyOnly + if ($LASTEXITCODE -ne 0) { throw 'Post-build Smart Discovery route verification failed.' } - - name: Upload P0-5g readiness evidence - if: always() - uses: actions/upload-artifact@v4 - with: - name: ARSAS-p0-5g-production-readiness - path: ArIED61850Tester\TestResults\P0-5G-*.json - if-no-files-found: warn - retention-days: 14 + $targets = Get-Content '.\ArIED61850Tester\Directory.Build.targets' -Raw + if ($targets -match 'GITHUB_WORKFLOW' -or $targets -match 'SmartDiscoveryProductionPromoted') { + throw 'Discovery runtime still depends on workflow name or promotion switch.' + } + Write-Host 'Production, R7, installer and local builds now compile the same tracked Smart Discovery route.' From 5968388a55d02c9561d394c4ec98541ab6b07ed9 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 20 Sep 2026 16:56:23 +0700 Subject: [PATCH 06/13] release: refuse legacy discovery route in Windows packages --- .github/workflows/release-windows.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/workflows/release-windows.yml b/.github/workflows/release-windows.yml index 3a2e1c271..181f20816 100644 --- a/.github/workflows/release-windows.yml +++ b/.github/workflows/release-windows.yml @@ -164,6 +164,27 @@ jobs: shell: powershell run: .\ArIED61850Tester\scripts\verify-source-clean.ps1 + - name: Verify physical-proven Smart Discovery route before release build + shell: powershell + run: | + $native = '.\ArIED61850Tester\Services\NativeIec61850Client.cs' + $before = (Get-FileHash $native -Algorithm SHA256).Hash + & .\ArIED61850Tester\scripts\enable-smart-discovery-capture.ps1 -VerifyOnly + if ($LASTEXITCODE -ne 0) { throw 'Release refuses a legacy/incomplete Smart Discovery route.' } + $after = (Get-FileHash $native -Algorithm SHA256).Hash + if ($before -ne $after) { throw 'Release verification mutated discovery source; refusing non-reproducible build.' } + + $source = Get-Content $native -Raw + foreach ($required in @( + 'return await DiscoverSignalsSmartForCaptureAsync(cancellationToken, progress).ConfigureAwait(false);', + '__P0_5C_CONNECT_RESET__', + '__P0_5C_DISPOSE_RESET__', + '_lastDiscovery.Snapshot.DomainVariables')) { + if ($source -notmatch [regex]::Escape($required)) { + throw "Release Smart Discovery contract missing: $required" + } + } + - name: Restore, build and test exact release source shell: powershell run: | From 4bf3980083d033bb6bdd1e0c4e1bdb59c34702d1 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 20 Sep 2026 16:56:26 +0700 Subject: [PATCH 07/13] installer: build only the physical-proven smart discovery route --- .github/workflows/installer-windows.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/workflows/installer-windows.yml b/.github/workflows/installer-windows.yml index 9b529905d..a5e5ea00d 100644 --- a/.github/workflows/installer-windows.yml +++ b/.github/workflows/installer-windows.yml @@ -19,6 +19,12 @@ on: - "tests/ARSAS.Tests/Comtrade*Tests.cs" - "ArIED61850Tester.csproj" - "Directory.Build.props" + - "Directory.Build.targets" + - "Services/NativeIec61850Client.cs" + - "Services/NativeIec61850Client.SmartDiscoveryCapture.cs" + - "Services/NativeIec61850Client.SmartDiscoveryLifecycle.cs" + - "scripts/enable-smart-discovery-capture.ps1" + - "tests/ARSAS.Tests/SmartDiscoveryProductionPromotionRegressionTests.cs" - "VERSION" - "engines/ARIEC61850.lock.json" - "engines/ARDIREC.lock.json" @@ -39,6 +45,12 @@ on: - "tests/ARSAS.Tests/Comtrade*Tests.cs" - "ArIED61850Tester.csproj" - "Directory.Build.props" + - "Directory.Build.targets" + - "Services/NativeIec61850Client.cs" + - "Services/NativeIec61850Client.SmartDiscoveryCapture.cs" + - "Services/NativeIec61850Client.SmartDiscoveryLifecycle.cs" + - "scripts/enable-smart-discovery-capture.ps1" + - "tests/ARSAS.Tests/SmartDiscoveryProductionPromotionRegressionTests.cs" - "VERSION" - "engines/ARIEC61850.lock.json" - "engines/ARDIREC.lock.json" @@ -117,6 +129,16 @@ jobs: with: dotnet-version: 8.0.x + - name: Verify physical-proven Smart Discovery route + shell: powershell + run: | + $native = '.\ArIED61850Tester\Services\NativeIec61850Client.cs' + $before = (Get-FileHash $native -Algorithm SHA256).Hash + & .\ArIED61850Tester\scripts\enable-smart-discovery-capture.ps1 -VerifyOnly + if ($LASTEXITCODE -ne 0) { throw 'Installer refuses a legacy/incomplete Smart Discovery route.' } + $after = (Get-FileHash $native -Algorithm SHA256).Hash + if ($before -ne $after) { throw 'Installer verification mutated discovery source.' } + - name: Restore build and test application solution shell: powershell run: | From 4eec760e081fd7921abfca09b56a169c49fb88eb Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 20 Sep 2026 16:56:42 +0700 Subject: [PATCH 08/13] ci: make R7 verify production-route parity instead of workflow gating --- .github/workflows/scl-interoperability-r7.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/scl-interoperability-r7.yml b/.github/workflows/scl-interoperability-r7.yml index 17a436da3..b7d06d0a6 100644 --- a/.github/workflows/scl-interoperability-r7.yml +++ b/.github/workflows/scl-interoperability-r7.yml @@ -191,8 +191,10 @@ jobs: $capture -match 'InitialFcReadPlanner\.FromSclModel' -or $capture -match 'ExecuteInitialFcReadPlanSmartAsync' -or $capture -notmatch 'initialFcRoots=deferred' -or - $buildTargets -notmatch 'SCL Interoperability R7 Build' -or - $buildTargets -notmatch 'EnableSmartDiscoveryCaptureRoute' -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 From e2a02ed1c251803bf980564b517945e815213544 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 20 Sep 2026 16:57:09 +0700 Subject: [PATCH 09/13] evidence: record v1.6.39 physical release rejection --- evidence/v1.6.39-physical-rejection.json | 48 ++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 evidence/v1.6.39-physical-rejection.json diff --git a/evidence/v1.6.39-physical-rejection.json b/evidence/v1.6.39-physical-rejection.json new file mode 100644 index 000000000..e7f7b675d --- /dev/null +++ b/evidence/v1.6.39-physical-rejection.json @@ -0,0 +1,48 @@ +{ + "schemaVersion": 1, + "release": "v1.6.39", + "status": "physically-rejected", + "testedAtLocal": "2026-09-20T16:45:33+07:00", + "relay": "AA1E1F06R4", + "applicationCommit": "638d5b1fbaf757041019bcfb6172a8b6766355a8", + "engineCommit": "648124097621046f5f127ceb1cf853fea54db730", + "observedPackaging": "installed multi-file package under C:\\Program Files\\ARSAS", + "rejection": { + "associationReadyLocal": "2026-09-20T16:44:53.912+07:00", + "smartScanCompleteLocal": "2026-09-20T16:45:25.534+07:00", + "discoveryElapsedSeconds": 31.622, + "staticMembers": 58, + "selectedStaticMembers": 58, + "analogMembers": 22, + "digitalMembers": 36, + "cyclicMmsProcessPolling": 0, + "reportBackedRuntimePoints": 23, + "expectedReportBackedRuntimePoints": 58, + "signature": [ + "REPORT_VALUE_REJECTED on Digital status points", + "runtime classified multiple report BIT STRING values as Boolean", + "legacy DiscoverSignalsAsync supplemental/probe path was compiled in production release instead of the physical-proven R7 Smart Discovery route" + ] + }, + "rootCause": { + "class": "build-route divergence", + "detail": "Directory.Build.targets enabled the physical-proven Smart Discovery route only for named R7/Field Capture workflows or a production-promotion switch that remained false. Release Windows therefore compiled the legacy discovery entrypoint even though the repository and engine SHAs matched the R7 candidate.", + "sourceShaEqualityWasInsufficient": true + }, + "correctivePolicy": { + "releaseBlockedUntilPhysicalRetest": true, + "nextStableVersion": "1.6.40", + "requireTrackedSmartRoute": true, + "forbidWorkflowNameDependentDiscoveryCompilation": true, + "requirePortableAndInstallerCandidatePhysicalRetest": true, + "acceptance": [ + "58/58 static members represented", + "22/22 Analog", + "36/36 Digital", + "58 report-backed runtime points", + "0 cyclic MMS process polling", + "no REPORT_VALUE_REJECTED BIT STRING-as-Boolean burst", + "physical-proven Smart Discovery timing/request shape restored" + ] + } +} From c4cc108e958de94adf410ae2a61cfabfa7e2a120 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 20 Sep 2026 16:59:03 +0700 Subject: [PATCH 10/13] ci: converge on one production smart discovery route --- .github/workflows/iedscout-convergence-guard.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/iedscout-convergence-guard.yml b/.github/workflows/iedscout-convergence-guard.yml index 9e977475f..e340266ff 100644 --- a/.github/workflows/iedscout-convergence-guard.yml +++ b/.github/workflows/iedscout-convergence-guard.yml @@ -276,8 +276,10 @@ jobs: throw 'P2 case-sensitive value path implementation regressed.' } - if ($targets -notmatch 'SCL Interoperability R7 Build' -or - $targets -notmatch 'EnableSmartDiscoveryCaptureRoute' -or + if ($targets -notmatch 'VerifyPhysicalProvenSmartDiscoveryRoute' -or + $targets -notmatch '\-VerifyOnly' -or + $targets -match 'GITHUB_WORKFLOW' -or + $targets -match 'SmartDiscoveryProductionPromoted' -or $capture -notmatch 'SmartDiscoveryStructuralFreezeContract = "P0-R9-STRUCTURAL"' -or $capture -notmatch 'CreateP0FrozenSmartDiscoveryOptions' -or $capture -notmatch 'MaxConcurrentChains = 8' -or From 425b0b9b66b73e21b47f0d01b9087d8086f4814c Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 20 Sep 2026 16:59:08 +0700 Subject: [PATCH 11/13] ci: make field capture verify production route parity --- .github/workflows/smart-discovery-capture-build.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/smart-discovery-capture-build.yml b/.github/workflows/smart-discovery-capture-build.yml index ed1756611..00ea1e8f4 100644 --- a/.github/workflows/smart-discovery-capture-build.yml +++ b/.github/workflows/smart-discovery-capture-build.yml @@ -70,7 +70,10 @@ jobs: $patcher -notmatch '__P0_5C_CONNECT_RESET__' -or $patcher -notmatch '__P0_5C_DISPOSE_RESET__' -or $patcher -notmatch '_lastDiscovery\.Snapshot\.DomainVariables' -or - $targets -notmatch 'EnableSmartDiscoveryCaptureRoute') { + $targets -notmatch 'VerifyPhysicalProvenSmartDiscoveryRoute' -or + $targets -notmatch '\-VerifyOnly' -or + $targets -match 'GITHUB_WORKFLOW' -or + $targets -match 'SmartDiscoveryProductionPromoted') { throw 'P0-5c association-scoped enrichment single-flight, authority publication, or lifecycle invalidation is incomplete.' } if ($helper -match 'ProbeSmartAsync\(_session, discovery\.IedDirectory, smartOptions, cancellationToken\)' -or From e18a335b0ecae2e2bedc4ed4dc76322a22328243 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 20 Sep 2026 17:02:25 +0700 Subject: [PATCH 12/13] test: require R7 and production build-route parity --- 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 67364653b..0dc8afeb2 100644 --- a/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs +++ b/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs @@ -341,8 +341,10 @@ public void R7Workflow_BindsArtifactToExactSourceHeadAndReloadContracts() StringComparison.Ordinal); var buildTargets = File.ReadAllText(FindRepoFile("Directory.Build.targets")); - Assert.Contains("SCL Interoperability R7 Build", buildTargets, StringComparison.Ordinal); - Assert.Contains("EnableSmartDiscoveryCaptureRoute", buildTargets, StringComparison.Ordinal); + Assert.Contains("VerifyPhysicalProvenSmartDiscoveryRoute", buildTargets, StringComparison.Ordinal); + Assert.Contains("-VerifyOnly", buildTargets, StringComparison.Ordinal); + Assert.DoesNotContain("GITHUB_WORKFLOW", buildTargets, StringComparison.Ordinal); + Assert.DoesNotContain("SmartDiscoveryProductionPromoted", buildTargets, StringComparison.Ordinal); Assert.Contains("evidence/iedscout-convergence-target.json", workflow, StringComparison.Ordinal); Assert.Contains("IEDScout convergence source contract regressed", workflow, StringComparison.Ordinal); Assert.Contains("TryResolveStandardSubDataObjectCdc", workflow, StringComparison.Ordinal); From a14c7f2c12ce48b29c269eb6096361076ce05be5 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 20 Sep 2026 17:02:29 +0700 Subject: [PATCH 13/13] test: lock association resets in tracked production source --- ...martDiscoveryAssociationSingleFlightRegressionTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/ARSAS.Tests/SmartDiscoveryAssociationSingleFlightRegressionTests.cs b/tests/ARSAS.Tests/SmartDiscoveryAssociationSingleFlightRegressionTests.cs index 0e002ac18..5a5d3d1f5 100644 --- a/tests/ARSAS.Tests/SmartDiscoveryAssociationSingleFlightRegressionTests.cs +++ b/tests/ARSAS.Tests/SmartDiscoveryAssociationSingleFlightRegressionTests.cs @@ -31,15 +31,15 @@ public void P05c_ReconnectAndDispose_InvalidateGenerationAndBlockStalePublish() { var lifecycle = File.ReadAllText(FindRepoFile("Services/NativeIec61850Client.SmartDiscoveryLifecycle.cs")); var capture = File.ReadAllText(FindRepoFile("Services/NativeIec61850Client.SmartDiscoveryCapture.cs")); - var patcher = File.ReadAllText(FindRepoFile("scripts/enable-smart-discovery-capture.ps1")); + var production = File.ReadAllText(FindRepoFile("Services/NativeIec61850Client.cs")); Assert.Contains("_smartDiscoveryAssociationGeneration++", lifecycle, StringComparison.Ordinal); Assert.Contains("generation != _smartDiscoveryAssociationGeneration", lifecycle, StringComparison.Ordinal); Assert.Contains("TryPublishSmartDiscoveryAuthority", lifecycle, StringComparison.Ordinal); Assert.Contains("IsCurrentSmartDiscoveryAssociationGeneration", capture, StringComparison.Ordinal); - Assert.Contains("__P0_5C_CONNECT_RESET__", patcher, StringComparison.Ordinal); - Assert.Contains("__P0_5C_DISPOSE_RESET__", patcher, StringComparison.Ordinal); - Assert.Contains("ResetSmartDiscoveryAuthority(); // __P0_5C_DISPOSE_RESET__", patcher, StringComparison.Ordinal); + Assert.Contains("ResetSmartDiscoveryAuthority(); // __P0_5C_CONNECT_RESET__", production, StringComparison.Ordinal); + Assert.Contains("ResetSmartDiscoveryAuthority(); // __P0_5C_DISPOSE_RESET__", production, StringComparison.Ordinal); + Assert.Contains("return await DiscoverSignalsSmartForCaptureAsync(cancellationToken, progress).ConfigureAwait(false);", production, StringComparison.Ordinal); } [Fact]