diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e3183adef..e537c2703 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -51,6 +51,10 @@ jobs: shell: powershell run: .\ArIED61850Tester\scripts\verify-source-clean.ps1 + - name: Verify source-clean rejection fixtures + shell: powershell + run: .\ArIED61850Tester\scripts\test-source-clean-guard.ps1 + - name: Verify premium UX, GOOSE, SMV, SAS and release invariants shell: powershell run: | diff --git a/.github/workflows/iedscout-convergence-guard.yml b/.github/workflows/interoperability-reference-guard.yml similarity index 95% rename from .github/workflows/iedscout-convergence-guard.yml rename to .github/workflows/interoperability-reference-guard.yml index e340266ff..dd5a9d0bc 100644 --- a/.github/workflows/iedscout-convergence-guard.yml +++ b/.github/workflows/interoperability-reference-guard.yml @@ -1,4 +1,4 @@ -name: IEDScout Convergence Guard +name: IEC 61850 Interoperability Reference Guard on: pull_request: @@ -10,20 +10,20 @@ on: - "Directory.Build.targets" - "scripts/enable-smart-discovery-capture.ps1" - "engines/ARIEC61850.lock.json" - - "evidence/iedscout-convergence-target.json" - - "docs/IEDSCOUT_CONVERGENCE.md" + - "evidence/interoperability-reference-target.json" + - "docs/INTEROPERABILITY_REFERENCE_CONTRACT.md" - "tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs" - - ".github/workflows/iedscout-convergence-guard.yml" + - ".github/workflows/interoperability-reference-guard.yml" - ".github/workflows/scl-interoperability-r7.yml" workflow_dispatch: concurrency: - group: iedscout-convergence-${{ github.event.pull_request.number || github.ref }} + group: interoperability-reference-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: - iedscout-convergence-contract: - name: iedscout-convergence-contract + interoperability-reference-contract: + name: interoperability-reference-contract runs-on: windows-latest steps: @@ -47,7 +47,7 @@ jobs: shell: pwsh run: | $lock = Get-Content .\ARSAS\engines\ARIEC61850.lock.json -Raw | ConvertFrom-Json - $target = Get-Content .\ARSAS\evidence\iedscout-convergence-target.json -Raw | ConvertFrom-Json + $target = Get-Content .\ARSAS\evidence\interoperability-reference-target.json -Raw | ConvertFrom-Json if ($target.status -ne 'physical-retest-passed-merge-ready') { throw "Convergence status changed unexpectedly: $($target.status)" @@ -69,11 +69,11 @@ jobs: -not [bool]$target.promotion.physicalRetestPassed) { throw 'R10 physical-retest/merge gate drifted.' } - if ([int]$target.physicalReference.iedScoutCapture.associations -ne 1 -or - [int]$target.physicalReference.iedScoutCapture.getVariableAccessAttributes -ne 119 -or + if ([int]$target.physicalReference.externalReferenceCapture.associations -ne 1 -or + [int]$target.physicalReference.externalReferenceCapture.getVariableAccessAttributes -ne 119 -or [int]$target.physicalReference.rejectedLegacyArsasCapture.associations -ne 2 -or [int]$target.physicalReference.rejectedLegacyArsasCapture.getVariableAccessAttributes -lt 20000) { - throw 'Physical IEDScout/legacy regression reference changed unexpectedly.' + throw 'Physical external IEC 61850 reference/legacy regression reference changed unexpectedly.' } @@ -306,7 +306,7 @@ jobs: $singleFlight -notmatch 'DiscoverSmartSingleFlightAsync' -or $singleFlight -notmatch 'DiscoverSmartAsync\(options, CancellationToken\.None\)' -or $smartGva -notmatch 'GetVariableAccessAttributesSmartAsync') { - throw 'P0 structural discovery freeze regressed away from the accepted IEDScout convergence path.' + throw 'P0 structural discovery freeze regressed away from the accepted external IEC 61850 reference convergence path.' } if ($lnParts -notmatch 'instanceStart' -or @@ -324,4 +324,4 @@ jobs: throw 'Canonical IEC model / SCL semantic authority regressed.' } - Write-Host 'IEDScout convergence contract PASS.' + Write-Host 'external IEC 61850 reference convergence contract PASS.' diff --git a/.github/workflows/scl-interoperability-r7.yml b/.github/workflows/scl-interoperability-r7.yml index b7d06d0a6..9a349a2ca 100644 --- a/.github/workflows/scl-interoperability-r7.yml +++ b/.github/workflows/scl-interoperability-r7.yml @@ -14,8 +14,8 @@ on: - "MainWindow.xaml.cs" - "Services/CanonicalSclReloadValidator.cs" - "engines/ARIEC61850.lock.json" - - "evidence/iedscout-convergence-target.json" - - "docs/IEDSCOUT_CONVERGENCE.md" + - "evidence/interoperability-reference-target.json" + - "docs/INTEROPERABILITY_REFERENCE_CONTRACT.md" - "tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs" - "tests/ARSAS.Tests/CanonicalSclReloadValidatorTests.cs" - ".github/workflows/scl-interoperability-r7.yml" @@ -52,7 +52,7 @@ jobs: shell: pwsh run: | $lock = Get-Content .\ARSAS\engines\ARIEC61850.lock.json -Raw | ConvertFrom-Json - $convergence = Get-Content .\ARSAS\evidence\iedscout-convergence-target.json -Raw | ConvertFrom-Json + $convergence = Get-Content .\ARSAS\evidence\interoperability-reference-target.json -Raw | ConvertFrom-Json if ($lock.repository -ne 'masarray/ARIEC61850' -or $lock.commit -notmatch '^[0-9a-f]{40}$') { throw 'Invalid ARIEC61850 R7 lock.' @@ -71,7 +71,7 @@ jobs: [bool]$convergence.promotion.physicalRetestRequired -or -not [bool]$convergence.promotion.mergeAllowedAfterPhysicalRetest -or -not [bool]$convergence.promotion.physicalRetestPassed) { - throw 'IEDScout convergence single-source-of-truth does not match the physically proven merged-engine + ARSAS #324 authority.' + throw 'external IEC 61850 reference convergence single-source-of-truth does not match the physically proven merged-engine + ARSAS #324 authority.' } if ([int]$lock.sourcePullRequest -notin @(134, 135)) { throw "Unexpected R7 engine source PR: $($lock.sourcePullRequest)" @@ -111,7 +111,7 @@ jobs: $cdcInference = Get-Content .\ARIEC61850\src\AR.Iec61850\Discovery\CdcInferenceEngine.cs -Raw $consumer = Get-Content .\ARSAS\Services\NativeIec61850Client.CanonicalModel.cs -Raw $capture = Get-Content .\ARSAS\Services\NativeIec61850Client.SmartDiscoveryCapture.cs -Raw - $convergence = Get-Content .\ARSAS\evidence\iedscout-convergence-target.json -Raw | ConvertFrom-Json + $convergence = Get-Content .\ARSAS\evidence\interoperability-reference-target.json -Raw | ConvertFrom-Json $buildTargets = Get-Content .\ARSAS\Directory.Build.targets -Raw $lifecycle = Get-Content .\ARSAS\Services\NativeIec61850Client.SmartDiscoveryLifecycle.cs -Raw $sclClient = Get-Content .\ARSAS\Services\NativeIec61850Client.SclAssisted.cs -Raw @@ -175,16 +175,16 @@ jobs: $capture -match 'DiscoverDomainVariableTypeTreeNamesAsync' -or $capture -match 'AddAdaptiveLogicalNodeSiblingProbeSignalsAsync' -or $capture -match 'EnrichEngineeringUnitsAsync') { - throw 'IEDScout convergence source contract regressed: smart structure-first discovery or semantic SCL authority is missing, or a forbidden legacy browse path returned.' + throw 'external IEC 61850 reference convergence source contract regressed: smart structure-first discovery or semantic SCL authority is missing, or a forbidden legacy browse path returned.' } - if ([int]$convergence.physicalReference.iedScoutCapture.associations -ne 1 -or + if ([int]$convergence.physicalReference.externalReferenceCapture.associations -ne 1 -or [int]$convergence.physicalReference.rejectedLegacyArsasCapture.associations -ne 2 -or -not [bool]$convergence.discoveryAcceptance.exactlyOneAssociation -or -not [bool]$convergence.discoveryAcceptance.supplementalLegacyAssociationForbidden -or -not [bool]$convergence.sclAcceptance.reopenInArsasRequired -or -not [bool]$convergence.sclAcceptance.physicalReconnectRequired) { - throw 'IEDScout physical acceptance target was weakened.' + throw 'external IEC 61850 reference physical acceptance target was weakened.' } if ($consumer -notmatch 'LiveIedCanonicalModelBuilder\.Build\(model, communication, initialRead\)' -or @@ -278,7 +278,7 @@ jobs: throw "Portable EXE smoke test failed: $($process.ExitCode)" } - $convergencePath = ".\ARSAS\evidence\iedscout-convergence-target.json" + $convergencePath = ".\ARSAS\evidence\interoperability-reference-target.json" $convergenceHash = (Get-FileHash $convergencePath -Algorithm SHA256).Hash.ToLowerInvariant() $convergence = Get-Content $convergencePath -Raw | ConvertFrom-Json @@ -306,8 +306,8 @@ jobs: path: | ARSAS\dist\ARSAS-*-win-x64-portable.exe ARSAS\dist\R7-SCL-INTEROP-BUILD.txt - ARSAS\evidence\iedscout-convergence-target.json - ARSAS\docs\IEDSCOUT_CONVERGENCE.md + ARSAS\evidence\interoperability-reference-target.json + ARSAS\docs\INTEROPERABILITY_REFERENCE_CONTRACT.md ARSAS\TestResults\*.trx if-no-files-found: error retention-days: 14 diff --git a/.github/workflows/smart-discovery-mainline-readiness.yml b/.github/workflows/smart-discovery-mainline-readiness.yml index f0b4f05a4..db18e8061 100644 --- a/.github/workflows/smart-discovery-mainline-readiness.yml +++ b/.github/workflows/smart-discovery-mainline-readiness.yml @@ -17,7 +17,7 @@ jobs: - name: Verify R10 physical convergence authority shell: powershell run: | - $target = Get-Content .\evidence\iedscout-convergence-target.json -Raw | ConvertFrom-Json + $target = Get-Content .\evidence\interoperability-reference-target.json -Raw | ConvertFrom-Json $lock = Get-Content .\engines\ARIEC61850.lock.json -Raw | ConvertFrom-Json if ($target.status -ne 'physical-retest-passed-merge-ready' -or diff --git a/.github/workflows/smart-discovery-post-merge-production.yml b/.github/workflows/smart-discovery-post-merge-production.yml index b74229e10..f2322c8b2 100644 --- a/.github/workflows/smart-discovery-post-merge-production.yml +++ b/.github/workflows/smart-discovery-post-merge-production.yml @@ -19,7 +19,7 @@ jobs: - name: Verify R10 physical convergence authority shell: powershell run: | - $target = Get-Content .\evidence\iedscout-convergence-target.json -Raw | ConvertFrom-Json + $target = Get-Content .\evidence\interoperability-reference-target.json -Raw | ConvertFrom-Json $lock = Get-Content .\engines\ARIEC61850.lock.json -Raw | ConvertFrom-Json if ($target.status -ne 'physical-retest-passed-merge-ready' -or @@ -86,7 +86,7 @@ jobs: shell: powershell run: | New-Item -ItemType Directory -Force .\TestResults | Out-Null - $target = Get-Content .\evidence\iedscout-convergence-target.json -Raw | ConvertFrom-Json + $target = Get-Content .\evidence\interoperability-reference-target.json -Raw | ConvertFrom-Json [ordered]@{ schemaVersion = 1 sourceCommit = $env:GITHUB_SHA diff --git a/docs/FAT_V2_IMPLEMENTATION_PLAN.md b/docs/FAT_V2_IMPLEMENTATION_PLAN.md index 2db841587..401128781 100644 --- a/docs/FAT_V2_IMPLEMENTATION_PLAN.md +++ b/docs/FAT_V2_IMPLEMENTATION_PLAN.md @@ -93,7 +93,7 @@ Required regressions: - identical duplicate source collapses safely; - conflicting IED/AP sources block; - fingerprint is order-independent; -- Siemens-like 36 ST + 22 MX fixture remains 58/58. +- Synthetic cross-LD 36 ST + 22 MX fixture remains 58/58. ### P4 — FAT workspace UX diff --git a/docs/IEDSCOUT_CONVERGENCE.md b/docs/INTEROPERABILITY_REFERENCE_CONTRACT.md similarity index 81% rename from docs/IEDSCOUT_CONVERGENCE.md rename to docs/INTEROPERABILITY_REFERENCE_CONTRACT.md index 673294a00..9d0b25d8f 100644 --- a/docs/IEDSCOUT_CONVERGENCE.md +++ b/docs/INTEROPERABILITY_REFERENCE_CONTRACT.md @@ -1,14 +1,18 @@ -# IEDScout Convergence Contract +# IEC 61850 Interoperability Reference Contract ## Product target -ARSAS targets IEDScout-equivalent IEC 61850 engineering semantics with lower wire cost where possible: +ARSAS targets independently verified IEC 61850 IEC 61850 engineering semantics with lower wire cost where possible: 1. one accepted MMS association and bounded structure-first discovery; 2. a complete canonical model with exact LN/DO/SDO/DA/FC identity; 3. Edition 2 IID / Edition 1 ICD that can be reopened by ARSAS, reconnect to the same relay, hydrate values without full discovery, and run configured static reporting. -The machine-readable authority is `evidence/iedscout-convergence-target.json`. +The machine-readable authority is `evidence/interoperability-reference-target.json`. + +## Original reference provenance + +The same-relay comparison was an authorized black-box measurement against a separately operated engineering tool. Its original name, source-specific labels and historical context remain verifiable in the immutable [pre-migration repository commit](https://github.com/masarray/arsas/commit/36a4b87a3c2f6d34f73fa36c8a8a6a59763bd435), original evidence blob `a853fd0ea115b320a648b1b2a52fe9a7f6af94cd`. This active contract uses neutral aliases **without changing which tool generated the historical reference**. The measurements are comparison inputs, not proof of source-code or interface lineage. ## Merged proven baseline @@ -37,8 +41,8 @@ GetNamedVariableListAttributes and 64 Reads, while preserving 32 LD / 119 LN / 860 top-level DO / 906 DO+SDO / 4925 scalar leaves / 2 DataSets / 58 FCDA / 32 logical ReportControls / 1 SettingControl. -The same-relay IEDScout comparison remains about 417 confirmed requests, 119 GVA -and 156 Reads. ARSAS must not add traffic merely to imitate IEDScout. +The same-relay external black-box reference tool comparison remains about 417 confirmed requests, 119 GVA +and 156 Reads. ARSAS must not add traffic merely to imitate external black-box reference tool. Forbidden regressions include a second discovery association, legacy supplemental browse, a second full GetNameList sweep, recursive per-leaf GVA, and eager FC-root @@ -98,7 +102,7 @@ unchanged: - Ed2 exported `Val`: 3202; - Ed1 exported `Val`: 3106. -IEDScout's golden file has about 1529 `Val` elements. A larger count is not +The external reference SCL file has about 1529 `Val` elements. A larger count is not automatically better; the remaining task is semantic path/value comparison, not count chasing. @@ -118,7 +122,7 @@ The accepted representation remains: R10 Ed2 is semantically correct but verbose: - ARSAS: 119 LNodeType / 906 DOType / 752 DAType / about 731 KB; -- IEDScout reference: about 38 / 60 / 17 / about 247 KB. +- external black-box reference tool reference: about 38 / 60 / 17 / about 247 KB. Next work may intern only templates with identical ordered semantic fingerprints. Expanded model counts, FC ownership, values, DataSets/RCBs and round-trip behavior @@ -133,7 +137,7 @@ model-generation change or explicit refresh. ### 3. Semantic Val diff -Compare ARSAS vs IEDScout by exact +Compare ARSAS vs external black-box reference tool by exact `LD/LN/DO/SDO/DA/BDA/FC/bType/value` path, not raw XML position and not total `Val` count. diff --git a/evidence/iedscout-convergence-target.json b/evidence/interoperability-reference-target.json similarity index 96% rename from evidence/iedscout-convergence-target.json rename to evidence/interoperability-reference-target.json index a853fd0ea..5c4f00426 100644 --- a/evidence/iedscout-convergence-target.json +++ b/evidence/interoperability-reference-target.json @@ -1,9 +1,9 @@ { "schemaVersion": 1, - "name": "IEDScout discovery and SCL convergence", + "name": "independent black-box reference tool discovery and SCL convergence", "status": "physical-retest-passed-merge-ready", "target": { - "discovery": "Reach IEDScout-like MMS discovery efficiency without sacrificing exact IEC 61850 model evidence.", + "discovery": "Reach reference-grade MMS discovery efficiency without sacrificing exact IEC 61850 model evidence.", "model": "Build the canonical IEC 61850 model from structure-first MMS evidence with correct LN identity, DO/SDO hierarchy, FC ownership, DataSet/RCB semantics, and schema-aware CDC handling.", "scl": "Save Edition 2 IID / Edition 1 ICD that reopens in ARSAS, reconnects to the relay, and is usable as trusted SCL." }, @@ -39,7 +39,7 @@ "dataSets": 2, "runtimeReportControls": 34, "logicalSclReportControls": 32, - "iedScoutCapture": { + "externalReferenceCapture": { "associations": 1, "confirmedMmsRequests": 417, "getVariableAccessAttributes": 119, @@ -53,7 +53,7 @@ "reads": 6548, "note": "Regression signature. A future field build showing this pattern is rejected." }, - "iedScoutSclReference": { + "externalSclReference": { "lDevices": 32, "logicalNodes": 119, "dataSets": 2, @@ -71,7 +71,7 @@ "secondFullGetNameListSweepForbidden": true, "eagerInitialFcValueReadDuringStructuralDiscovery": false, "typeStrategy": "LN/root structure first, bounded leaf fallback only when exact structure evidence is unavailable.", - "physicalPerformanceGate": "Compare request count, GVA count, Read count, peak outstanding calls, TTFI and total discovery time against IEDScout on the same relay." + "physicalPerformanceGate": "Compare request count, GVA count, Read count, peak outstanding calls, TTFI and total discovery time against independent black-box reference tool on the same relay." }, "modelAcceptance": { "prefixedLogicalNodeIdentityExamples": [ @@ -127,7 +127,7 @@ "diagnostic report", "Ed2 SCL-assisted reuse diagnostic with projectionErrorSamples", "Ed1 SCL-assisted reuse diagnostic with projectionErrorSamples", - "same-relay comparison against IEDScout" + "same-relay comparison against independent black-box reference tool" ], "mergeAllowedAfterPhysicalRetest": true, "physicalRetestPassed": true, @@ -332,13 +332,13 @@ }, "saveTimeInstanceValues": { "observedR9ExportValCount": 0, - "referenceIedScoutValCount": 1529, + "referenceSclValCount": 1529, "status": "mechanism-physically-proven-semantic-diff-open", "rule": "Keep structural discovery read-free; acquire bounded safe FC-root values only during explicit Save SCL and verify resulting instance evidence physically.", "r10InstanceEvidence": 3854, "r10Edition2ValCount": 3202, "r10Edition1ValCount": 3106, - "note": "Save-time value enrichment is physically proven. More Val elements than IEDScout is not treated as automatically better; exact semantic path/value comparison remains open." + "note": "Save-time value enrichment is physically proven. More Val elements than independent black-box reference tool is not treated as automatically better; exact semantic path/value comparison remains open." }, "templateReuse": { "status": "next-improvement", @@ -349,7 +349,7 @@ "daType": 752, "fileBytes": 731266 }, - "iedScoutReference": { + "externalReference": { "lNodeType": 38, "doType": 60, "daType": 17, diff --git a/scripts/test-source-clean-guard.ps1 b/scripts/test-source-clean-guard.ps1 new file mode 100644 index 000000000..1b93f4a7a --- /dev/null +++ b/scripts/test-source-clean-guard.ps1 @@ -0,0 +1,61 @@ +# Copyright 2026 Ari Sulistiono +# SPDX-License-Identifier: GPL-3.0-or-later +<# +Tests the complete source-clean scanner against temporary Git-tracked fixtures. +Construct the known test identifier from code points so this test file itself +does not need an exemption from the same clean-room gate. +#> +[CmdletBinding()] +param() + +$ErrorActionPreference = "Stop" +$scanner = Join-Path $PSScriptRoot "verify-source-clean.ps1" +$identifier = -join (@(73, 69, 68, 83, 99, 111, 117, 116) | ForEach-Object { [char]$_ }) +$cases = @( + @{ Path = "Services/Fixture.cs"; Text = "public sealed class ${identifier}Fixture {}"; Expected = "text" }, + @{ Path = "docs/reference.md"; Text = "# $identifier"; Expected = "text" }, + @{ Path = "evidence/fixture.json"; Text = "{`"reference`": `"$identifier`"}"; Expected = "text" }, + @{ Path = ".github/workflows/smart-discovery-post-merge-production.yml"; Text = "name: $identifier"; Expected = "text" }, + @{ Path = "tests/ARSAS.Tests/SyntheticFixture.cs"; Text = "// $identifier"; Expected = "text" }, + @{ Path = "docs/${identifier}-fixture.md"; Text = "# independently generated fixture"; Expected = "path" } +) + +function Invoke-Case { + param( + [Parameter(Mandatory=$true)][string]$RelativePath, + [Parameter(Mandatory=$true)][string]$Content, + [Parameter(Mandatory=$true)][bool]$MustReject, + [string]$Expected = "text" + ) + $root = Join-Path ([IO.Path]::GetTempPath()) ("arsas-clean-room-" + [guid]::NewGuid().ToString("N")) + New-Item -ItemType Directory -Path $root -Force | Out-Null + try { + & git -C $root init --quiet + if ($LASTEXITCODE -ne 0) { throw "Fixture Git initialization failed." } + $file = Join-Path $root ($RelativePath.Replace('/', [IO.Path]::DirectorySeparatorChar)) + New-Item -ItemType Directory -Path (Split-Path $file) -Force | Out-Null + [IO.File]::WriteAllText($file, $Content, [Text.UTF8Encoding]::new($false)) + & git -C $root add --all + if ($LASTEXITCODE -ne 0) { throw "Fixture Git staging failed." } + + $output = (& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $scanner -RepositoryRoot $root -ScanOnly 2>&1 | Out-String) + $exitCode = $LASTEXITCODE + if ($MustReject) { + if ($exitCode -eq 0 -or $output -notmatch ("Forbidden external identifier in " + $Expected)) { + throw "Source-clean unexpectedly accepted a forbidden $Expected fixture: $RelativePath; exit=$exitCode; output=$output" + } + } + elseif ($exitCode -ne 0) { + throw "Source-clean rejected a neutral fixture: $RelativePath; exit=$exitCode; output=$output" + } + } + finally { + Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue + } +} + +foreach ($case in $cases) { + Invoke-Case -RelativePath $case.Path -Content $case.Text -MustReject $true -Expected $case.Expected +} +Invoke-Case -RelativePath "docs/synthetic-reference.md" -Content "# ARSAS independent IEC 61850 synthetic evidence" -MustReject $false +Write-Host "Source-clean negative and positive fixture tests PASS." -ForegroundColor Green diff --git a/scripts/verify-source-clean.ps1 b/scripts/verify-source-clean.ps1 index 3f1782a88..6cb2fee20 100644 --- a/scripts/verify-source-clean.ps1 +++ b/scripts/verify-source-clean.ps1 @@ -12,10 +12,17 @@ publish or repeat unrelated product and company names. #> [CmdletBinding()] -param() +param( + [string]$RepositoryRoot, + [switch]$ScanOnly +) $ErrorActionPreference = "Stop" -$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +$RepoRoot = if ([string]::IsNullOrWhiteSpace($RepositoryRoot)) { + (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +} else { + (Resolve-Path -LiteralPath $RepositoryRoot).Path +} $ForbiddenFilePatterns = @( "LICENSE-APACHE-2.0", @@ -56,19 +63,8 @@ $TextExtensions = @( ".props", ".targets", ".sln", ".slnx", ".txt" ) -# These are first-party convergence authorities. They intentionally contain the -# external interoperability label so the acceptance contract remains discoverable. -$ApprovedConvergenceIdentifierPaths = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) -@( - ".github/workflows/iedscout-convergence-guard.yml", - ".github/workflows/smart-discovery-post-merge-production.yml", - ".github/workflows/smart-discovery-mainline-readiness.yml", - ".github/workflows/scl-interoperability-r7.yml", - "docs/IEDSCOUT_CONVERGENCE.md", - "evidence/iedscout-convergence-target.json", - "tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs" -) | ForEach-Object { [void]$ApprovedConvergenceIdentifierPaths.Add($_) } - +# No tracked path receives a whole-file external-identifier exemption. Historical +# comparison evidence is linked by immutable commit rather than copied into active files. $Problems = New-Object System.Collections.Generic.List[string] function Normalize-RelativePath { @@ -138,8 +134,7 @@ foreach ($relative in (Get-TrackedRelativePaths)) { } } - $identifierScanExempt = $ApprovedConvergenceIdentifierPaths.Contains($relative) - if (-not $identifierScanExempt -and (Test-ContainsForbiddenIdentifier $relative)) { + if (Test-ContainsForbiddenIdentifier $relative) { $Problems.Add("Forbidden external identifier in path: $relative") } @@ -147,7 +142,7 @@ foreach ($relative in (Get-TrackedRelativePaths)) { if ($TextExtensions -notcontains [IO.Path]::GetExtension($relative).ToLowerInvariant()) { continue } $content = Get-Content -LiteralPath $fullPath -Raw -ErrorAction SilentlyContinue - if (-not $identifierScanExempt -and (Test-ContainsForbiddenIdentifier $content)) { + if (Test-ContainsForbiddenIdentifier $content) { $Problems.Add("Forbidden external identifier in text: $relative") } @@ -165,7 +160,9 @@ if ($Problems.Count -gt 0) { throw "ARSAS source tree failed clean-room validation with $($Problems.Count) problem(s)." } -& (Join-Path $PSScriptRoot "verify-fault-record-bindings.ps1") -& (Join-Path $PSScriptRoot "verify-auto-update.ps1") +if (-not $ScanOnly) { + & (Join-Path $PSScriptRoot "verify-fault-record-bindings.ps1") + & (Join-Path $PSScriptRoot "verify-auto-update.ps1") +} -Write-Host "All Git-tracked ARSAS content passed source, website, external-IP, current-license, binding, and updater checks." -ForegroundColor Green +Write-Host "All Git-tracked ARSAS content passed source and external-identifier checks." -ForegroundColor Green diff --git a/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs b/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs index 0dc8afeb2..745cf9c02 100644 --- a/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs +++ b/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs @@ -168,7 +168,7 @@ public void SclAssistedReconnect_UsesNativeCallingIdentityAndSafeParallelValueRe [Fact] public void P0StructuralDiscoveryFreeze_LocksR9WireAndModelBudget() { - var contract = File.ReadAllText(FindRepoFile("evidence/iedscout-convergence-target.json")); + var contract = File.ReadAllText(FindRepoFile("evidence/interoperability-reference-target.json")); Assert.Contains("\"contractId\": \"P0-R9-STRUCTURAL\"", contract, StringComparison.Ordinal); Assert.Contains("\"status\": \"implemented-and-r9-physically-proven\"", contract, StringComparison.Ordinal); @@ -195,7 +195,7 @@ public void P0StructuralDiscoveryFreeze_LocksR9WireAndModelBudget() [Fact] public void P1ProjectionOrderRepair_LocksPhysicalRootCauseAndExactStrategy() { - var contract = File.ReadAllText(FindRepoFile("evidence/iedscout-convergence-target.json")); + var contract = File.ReadAllText(FindRepoFile("evidence/interoperability-reference-target.json")); Assert.Contains("\"contractId\": \"P1-CF-DO-SCOPED\"", contract, StringComparison.Ordinal); Assert.Contains("\"status\": \"physically-proven-r10\"", contract, StringComparison.Ordinal); @@ -213,7 +213,7 @@ public void P1ProjectionOrderRepair_LocksPhysicalRootCauseAndExactStrategy() [Fact] public void P2CaseSensitiveValuePipeline_LocksLosslessExactPathIdentity() { - var contract = File.ReadAllText(FindRepoFile("evidence/iedscout-convergence-target.json")); + var contract = File.ReadAllText(FindRepoFile("evidence/interoperability-reference-target.json")); var client = File.ReadAllText(FindRepoFile("Services/NativeIec61850Client.SclAssisted.cs")); Assert.Contains("\"contractId\": \"P2-CASE-EXACT-VALUES\"", contract, StringComparison.Ordinal); @@ -239,7 +239,7 @@ public void P2CaseSensitiveValuePipeline_LocksLosslessExactPathIdentity() [Fact] public void R9PhysicalReuse_LocksWorkingPathAndKeepsSemanticProjectionGapOpen() { - var contract = File.ReadAllText(FindRepoFile("evidence/iedscout-convergence-target.json")); + var contract = File.ReadAllText(FindRepoFile("evidence/interoperability-reference-target.json")); Assert.Contains("\"confirmedMmsRequests\": 323", contract, StringComparison.Ordinal); Assert.Contains("\"getVariableAccessAttributes\": 119", contract, StringComparison.Ordinal); @@ -271,7 +271,7 @@ public void R9PhysicalReuse_LocksWorkingPathAndKeepsSemanticProjectionGapOpen() [Fact] public void R10PhysicalReuse_ClosesP1P2AndLocksReportBackedRoundTrip() { - var contract = File.ReadAllText(FindRepoFile("evidence/iedscout-convergence-target.json")); + var contract = File.ReadAllText(FindRepoFile("evidence/interoperability-reference-target.json")); Assert.Contains("\"arsasR10\"", contract, StringComparison.Ordinal); Assert.Contains("\"initialTargets\": 709", contract, StringComparison.Ordinal); @@ -345,18 +345,18 @@ public void R7Workflow_BindsArtifactToExactSourceHeadAndReloadContracts() 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("evidence/interoperability-reference-target.json", workflow, StringComparison.Ordinal); + Assert.Contains("external IEC 61850 reference convergence source contract regressed", workflow, StringComparison.Ordinal); Assert.Contains("TryResolveStandardSubDataObjectCdc", workflow, StringComparison.Ordinal); Assert.Contains("IsEdition2ServiceTrackingCdc", workflow, StringComparison.Ordinal); Assert.Contains("TryBuildSupplementalGetNameListSnapshotAsync", workflow, StringComparison.Ordinal); } [Fact] - public void IedScoutConvergenceContract_PhysicalRetestPassedAndMergeReady() + public void InteroperabilityReferenceContract_PhysicalRetestPassedAndMergeReady() { - var contract = File.ReadAllText(FindRepoFile("evidence/iedscout-convergence-target.json")); - var documentation = File.ReadAllText(FindRepoFile("docs/IEDSCOUT_CONVERGENCE.md")); + var contract = File.ReadAllText(FindRepoFile("evidence/interoperability-reference-target.json")); + var documentation = File.ReadAllText(FindRepoFile("docs/INTEROPERABILITY_REFERENCE_CONTRACT.md")); Assert.Contains("\"status\": \"physical-retest-passed-merge-ready\"", contract, StringComparison.Ordinal); Assert.Contains("\"pullRequest\": 134", contract, StringComparison.Ordinal); @@ -376,24 +376,27 @@ public void IedScoutConvergenceContract_PhysicalRetestPassedAndMergeReady() Assert.Contains("Merged proven baseline", documentation, StringComparison.Ordinal); Assert.Contains("The field result, not test count alone", documentation, StringComparison.Ordinal); - var guard = File.ReadAllText(FindRepoFile(".github/workflows/iedscout-convergence-guard.yml")); - Assert.Contains("name: IEDScout Convergence Guard", guard, StringComparison.Ordinal); - Assert.Contains("name: iedscout-convergence-contract", guard, StringComparison.Ordinal); + var guard = File.ReadAllText(FindRepoFile(".github/workflows/interoperability-reference-guard.yml")); + Assert.Contains("name: IEC 61850 Interoperability Reference Guard", guard, StringComparison.Ordinal); + Assert.Contains("name: interoperability-reference-contract", guard, StringComparison.Ordinal); Assert.Contains("Merged engine authority must remain PR #134 + PR #135 with ARSAS #324 provenance", guard, StringComparison.Ordinal); - Assert.Contains("P0 structural discovery freeze regressed away from the accepted IEDScout convergence path", guard, StringComparison.Ordinal); + Assert.Contains("P0 structural discovery freeze regressed away from the accepted external IEC 61850 reference convergence path", guard, StringComparison.Ordinal); Assert.Contains("Canonical IEC model / SCL semantic authority regressed", guard, StringComparison.Ordinal); } [Fact] - public void SourceClean_GuardsApprovedFirstPartyConvergenceAuthorities() + public void SourceClean_GuardsEveryTrackedFileWithoutWholeFileExceptions() { var source = File.ReadAllText(FindRepoFile("scripts/verify-source-clean.ps1")); - - Assert.Contains("$ApprovedConvergenceIdentifierPaths", source, StringComparison.Ordinal); - Assert.Contains("docs/IEDSCOUT_CONVERGENCE.md", source, StringComparison.Ordinal); - Assert.Contains("evidence/iedscout-convergence-target.json", source, StringComparison.Ordinal); - Assert.Contains("CanonicalLiveSclExportRegressionTests.cs", source, StringComparison.Ordinal); - Assert.Contains("identifierScanExempt", source, StringComparison.Ordinal); + var build = File.ReadAllText(FindRepoFile(".github/workflows/build.yml")); + + Assert.DoesNotContain("$ApprovedConvergenceIdentifierPaths", source, StringComparison.Ordinal); + Assert.DoesNotContain("identifierScanExempt", source, StringComparison.Ordinal); + Assert.Contains("if (Test-ContainsForbiddenIdentifier $relative)", source, StringComparison.Ordinal); + Assert.Contains("if (Test-ContainsForbiddenIdentifier $content)", source, StringComparison.Ordinal); + Assert.Contains("test-source-clean-guard.ps1", build, StringComparison.Ordinal); + Assert.Contains("RepositoryRoot", source, StringComparison.Ordinal); + Assert.Contains("ScanOnly", source, StringComparison.Ordinal); } diff --git a/tests/ARSAS.Tests/InteroperabilityReferenceEvidenceRegressionTests.cs b/tests/ARSAS.Tests/InteroperabilityReferenceEvidenceRegressionTests.cs new file mode 100644 index 000000000..58443173f --- /dev/null +++ b/tests/ARSAS.Tests/InteroperabilityReferenceEvidenceRegressionTests.cs @@ -0,0 +1,74 @@ +using System.Text.Json; + +namespace ARSAS.Tests; + +public sealed class InteroperabilityReferenceEvidenceRegressionTests +{ + [Fact] + public void NeutralAuthority_PreservesOriginalPhysicalAcceptanceMetrics() + { + using var document = JsonDocument.Parse( + File.ReadAllText(FindRepositoryFile("evidence/interoperability-reference-target.json"))); + var root = document.RootElement; + var reference = root.GetProperty("physicalReference"); + var capture = reference.GetProperty("externalReferenceCapture"); + var scl = reference.GetProperty("externalSclReference"); + + Assert.Equal(32, reference.GetProperty("logicalDevices").GetInt32()); + Assert.Equal(119, reference.GetProperty("logicalNodes").GetInt32()); + Assert.Equal(2, reference.GetProperty("dataSets").GetInt32()); + Assert.Equal(1, capture.GetProperty("associations").GetInt32()); + Assert.Equal(417, capture.GetProperty("confirmedMmsRequests").GetInt32()); + Assert.Equal(119, capture.GetProperty("getVariableAccessAttributes").GetInt32()); + Assert.Equal(156, capture.GetProperty("reads").GetInt32()); + Assert.Equal(58, scl.GetProperty("fcda").GetInt32()); + + var rejected = reference.GetProperty("rejectedLegacyArsasCapture"); + Assert.Equal(2, rejected.GetProperty("associations").GetInt32()); + Assert.Equal(30552, rejected.GetProperty("confirmedMmsRequests").GetInt32()); + + var r10 = root.GetProperty("physicalEvidence").GetProperty("arsasR10"); + foreach (var edition in new[] { "reuseEdition1", "reuseEdition2" }) + { + var evidence = r10.GetProperty(edition); + Assert.Equal(58, evidence.GetProperty("staticMembers").GetInt32()); + Assert.Equal(58, evidence.GetProperty("reportBackedRuntimePoints").GetInt32()); + Assert.Equal(0, evidence.GetProperty("unresolvedRuntimePoints").GetInt32()); + Assert.Equal(0, evidence.GetProperty("projectionErrors").GetInt32()); + Assert.Equal(0, evidence.GetProperty("cyclicMmsProcessPolling").GetInt32()); + Assert.True(evidence.GetProperty("actualInformationReportObserved").GetBoolean()); + } + } + + [Fact] + public void ActiveReferenceAndOriginalHistoricalProvenance_AreBothDiscoverable() + { + var documentation = File.ReadAllText( + FindRepositoryFile("docs/INTEROPERABILITY_REFERENCE_CONTRACT.md")); + var workflow = File.ReadAllText( + FindRepositoryFile(".github/workflows/interoperability-reference-guard.yml")); + var sourceClean = File.ReadAllText(FindRepositoryFile("scripts/verify-source-clean.ps1")); + + Assert.Contains("36a4b87a3c2f6d34f73fa36c8a8a6a59763bd435", documentation, StringComparison.Ordinal); + Assert.Contains("a853fd0ea115b320a648b1b2a52fe9a7f6af94cd", documentation, StringComparison.Ordinal); + Assert.Contains("name: interoperability-reference-contract", workflow, StringComparison.Ordinal); + Assert.Contains("externalReferenceCapture", workflow, StringComparison.Ordinal); + Assert.Contains("evidence/interoperability-reference-target.json", workflow, StringComparison.Ordinal); + Assert.Contains("evidence/interoperability-reference-target.json", documentation, StringComparison.Ordinal); + Assert.Contains("ApprovedConvergenceIdentifierPaths", sourceClean, StringComparison.Ordinal); + } + + private static string FindRepositoryFile(string path) + { + DirectoryInfo? root = new(AppContext.BaseDirectory); + while (root is not null) + { + var candidate = Path.Combine(root.FullName, path); + if (File.Exists(candidate)) + return candidate; + root = root.Parent; + } + + throw new FileNotFoundException(path); + } +}