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
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: |
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: |
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
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
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.'
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'">
+
+
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;
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"
+ ]
+ }
+}
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 '; ')
+"@
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);
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]
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]