Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6d579f7
test(interop): pin code-verified P0-P6.1 engine trial for #446
masarray Oct 7, 2026
d051a1c
feat(interop): add thin canonical acquisition consumer boundary (#446)
masarray Oct 7, 2026
3087869
refactor(scl): replace fabricated trusted RCB inventory with canonica…
masarray Oct 7, 2026
fc3c31b
refactor(reporting): consume engine canonical static smart path (#446)
masarray Oct 7, 2026
b2cfc50
refactor(reporting): activate static plans without rediscovery (#446)
masarray Oct 7, 2026
951bc87
test(interop): lock thin ARSAS canonical static consumer contract (#446)
masarray Oct 7, 2026
1c35848
test(interop): update static path contracts to engine-owned P7 flow (…
masarray Oct 7, 2026
384880e
test(reporting): align report-only regression with engine canonical c…
masarray Oct 7, 2026
3d10811
test(ci): align mainline guard wording with accepted engine authority
masarray Oct 7, 2026
290d5ba
test(interop): preserve physical baseline while allowing explicit cod…
masarray Oct 7, 2026
06aecfd
refactor(runtime): converge Open SCL and Discovery static activation …
masarray Oct 7, 2026
382910f
test(runtime): lock one activation path for Open SCL and Discovery (#…
masarray Oct 7, 2026
b4ee058
refactor(runtime): converge Open SCL and Discovery static planning pa…
masarray Oct 7, 2026
c04dd11
refactor(reporting): remove obsolete ARSAS RCB availability ranking (…
masarray Oct 7, 2026
e859ec2
refactor(scl): remove retired trusted-SCL RCB runtime cache (#446)
masarray Oct 7, 2026
70061f5
refactor(reporting): remove parallel trusted-SCL reporting runtime (#…
masarray Oct 7, 2026
3914a3a
test(reporting): retire duplicate trusted-SCL activation contract (#446)
masarray Oct 7, 2026
43290c3
test(scl): bind golden-wire contract to unified configured-static run…
masarray Oct 7, 2026
825902f
ci(interoperability): separate code-trial engine from physical ancest…
masarray Oct 7, 2026
0203526
ci(readiness): allow stacked CodeVerified trial without weakening mai…
masarray Oct 7, 2026
271ad92
test(ci): lock stacked trial vs mainline physical authority semantics…
masarray Oct 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions .github/workflows/interoperability-reference-guard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,10 @@ jobs:
$association.status -ne 'physically-accepted-engine-merged-consumer-pending-merge' -or
-not [bool]$association.preservesR10Baseline -or
[int]$association.engine.pullRequest -ne 143 -or
$association.engine.testedHead -ne $lock.commit -or
$association.engine.testedHead -ne $lock.sclAssociationInteroperability.testedEngineCommit -or
$association.engine.mergedMain -ne $lock.sclAssociationInteroperability.mergedEngineCommit -or
$association.engine.tree -ne $lock.sclAssociationInteroperability.engineTree -or
[int]$lock.sourcePullRequest -ne 143 -or
$lock.sclAssociationInteroperability.testedEngineCommit -ne $lock.commit -or
[int]$lock.sclAssociationInteroperability.sourcePullRequest -ne 143 -or
-not [bool]$association.physicalAcceptance.fullDiscoverySkipped -or
[int]$association.physicalAcceptance.associationAttempts -ne 1 -or
[int]$association.physicalAcceptance.matchedDomains -ne 5 -or
Expand All @@ -85,6 +84,26 @@ jobs:
-not [bool]$association.physicalAcceptance.actualInformationReportObserved) {
throw 'Accepted post-baseline SCL association authority drifted or lost physical evidence.'
}
$trial = $lock.smartInteroperabilityCodeTrial
if ($null -ne $trial) {
if ($trial.status -ne 'code-verified-not-physical' -or
[int]$trial.issue -ne 144 -or
[int]$trial.integrationPullRequest -ne 148 -or
$trial.exactCommit -ne $lock.commit -or
[int]$lock.sourcePullRequest -ne [int]$trial.integrationPullRequest -or
-not [bool]$trial.fullDiscoveryFallbackForbidden -or
-not [bool]$trial.vendorBranchingForbidden -or
-not [bool]$trial.physicalPromotionRequired) {
throw 'Smart interoperability code-trial authority is malformed or was silently promoted.'
}

if ($lock.sclAssociationInteroperability.testedEngineCommit -ne $association.engine.testedHead -or
$lock.sclAssociationInteroperability.mergedEngineCommit -ne $association.engine.mergedMain -or
$lock.sclAssociationInteroperability.engineTree -ne $association.engine.tree) {
throw 'Code trial replaced or mutated the physically accepted SCL-association ancestry.'
}
}

if ([bool]$target.promotion.productionPromoted -or
[bool]$target.promotion.mergeAllowedBeforePhysicalRetest -or
[bool]$target.promotion.physicalRetestRequired -or
Expand Down
31 changes: 28 additions & 3 deletions .github/workflows/smart-discovery-mainline-readiness.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ jobs:

- name: Verify R10 physical convergence authority
shell: powershell
env:
PR_BASE_REF: ${{ github.base_ref }}
run: |
$target = Get-Content .\evidence\interoperability-reference-target.json -Raw | ConvertFrom-Json
$lock = Get-Content .\engines\ARIEC61850.lock.json -Raw | ConvertFrom-Json
Expand Down Expand Up @@ -82,9 +84,11 @@ jobs:
if ($association.contractId -ne 'SCL-ASSOC-PARTIAL-ADDR-PHYSICAL' -or
$association.status -ne 'physically-accepted-engine-merged-consumer-pending-merge' -or
-not [bool]$association.preservesR10Baseline -or
$association.engine.testedHead -ne $lock.commit -or
[int]$association.engine.pullRequest -ne [int]$lock.sourcePullRequest -or
[int]$lock.sourcePullRequest -ne 143 -or
$association.engine.testedHead -ne $lock.sclAssociationInteroperability.testedEngineCommit -or
[int]$association.engine.pullRequest -ne [int]$lock.sclAssociationInteroperability.sourcePullRequest -or
[int]$lock.sclAssociationInteroperability.sourcePullRequest -ne 143 -or
$association.engine.mergedMain -ne $lock.sclAssociationInteroperability.mergedEngineCommit -or
$association.engine.tree -ne $lock.sclAssociationInteroperability.engineTree -or
-not [bool]$association.physicalAcceptance.fullDiscoverySkipped -or
[int]$association.physicalAcceptance.failedReads -ne 0 -or
[int]$association.physicalAcceptance.projectionErrors -ne 0 -or
Expand All @@ -94,6 +98,27 @@ jobs:
throw 'Post-baseline SCL association acceptance is missing or inconsistent.'
}

$trial = $lock.smartInteroperabilityCodeTrial
if ($null -ne $trial -and $lock.commit -eq $trial.exactCommit) {
if ($trial.status -ne 'code-verified-not-physical' -or
[int]$trial.issue -ne 144 -or
[int]$trial.integrationPullRequest -ne 148 -or
[int]$lock.sourcePullRequest -ne [int]$trial.integrationPullRequest -or
-not [bool]$trial.physicalPromotionRequired) {
throw 'Smart interoperability code-trial metadata is inconsistent.'
}

if ($env:PR_BASE_REF -eq 'main') {
throw 'CodeVerified smart-interoperability trial cannot pass mainline readiness before fresh physical qualification.'
}

Write-Host "Stacked consumer trial accepted for CI against $($trial.exactCommit); historical physical authority remains $($association.engine.testedHead)."
}
elseif ($lock.commit -ne $association.engine.testedHead -or
[int]$lock.sourcePullRequest -ne 143) {
throw 'Runtime engine pin is neither the physically accepted authority nor an explicit stacked CodeVerified trial.'
}

"ENGINE_REPOSITORY=$($lock.repository)" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append
"ENGINE_COMMIT=$($lock.commit)" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append

Expand Down
26 changes: 7 additions & 19 deletions Services/Iec61850MonitorRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -781,11 +781,13 @@ private async Task StartReportPlansAsync(
cancellationToken.ThrowIfCancellationRequested();
try
{
var result = session.StaticDataSetReportOnly && session.Client.HasTrustedSclOnlineAuthority
? await session.Client.StartTrustedSclStaticReportMonitorAsync(plan, cancellationToken).ConfigureAwait(false)
: plan.IsEngineAuthoritative
? await session.Client.StartHybridReportMonitorAsync(plan, cancellationToken).ConfigureAwait(false)
: await session.Client.StartReportMonitorAsync(plan, cancellationToken).ConfigureAwait(false);
// Discovery IP and Open SCL converge before execution. Once ARIEC has
// produced an engine-authoritative plan, both sources use the exact same
// report activation runtime; source provenance is diagnostics, not a second
// transport/reporting implementation.
var result = plan.IsEngineAuthoritative
? await session.Client.StartHybridReportMonitorAsync(plan, cancellationToken).ConfigureAwait(false)
: await session.Client.StartReportMonitorAsync(plan, cancellationToken).ConfigureAwait(false);
session.HybridValidation.RecordActivation(plan, result);
if (!result.IsSuccess)
{
Expand Down Expand Up @@ -1015,20 +1017,6 @@ private async Task<IReadOnlyList<ReportControlPlan>> BuildReportPlansForCurrentA
IReadOnlyList<ReportControlPlan> legacyPlans,
CancellationToken cancellationToken)
{
if (session.StaticDataSetReportOnly && session.Client.HasTrustedSclOnlineAuthority)
{
session.HybridValidation.Reset(null);
var trustedPlans = legacyPlans.Count > 0
? legacyPlans
: Iec61850ReportPlanner.BuildPlans(
session.Device,
session.Points.Values,
allowDynamicDataSetWrites: false);
Log("INFO", session.Device.Name,
$"Trusted SCL report planning retained {trustedPlans.Count} local static candidate(s). DataSet membership and RCB identity remain SCL-authoritative; online directory discovery and Hybrid availability probing are bypassed.");
return trustedPlans.Where(plan => !plan.AllowDynamicDataSetWrites).ToArray();
}

if (session.Client.CanUseHybridReportPlanner(session.Device))
{
NativeHybridReportPlanningResult hybrid;
Expand Down
157 changes: 157 additions & 0 deletions Services/NativeIec61850Client.CanonicalAcquisition.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
using System.Xml.Linq;
using ArCanonical = AR.Iec61850.Engineering.Canonical;
using ArSclEngineering = AR.Iec61850.Scl.Engineering;
using ArMms = AR.Iec61850.Mms;
using ArIED61850Tester.Models;

namespace ArIED61850Tester.Services;

/// <summary>
/// Thin application boundary for the source-neutral ARIEC canonical model.
///
/// ARSAS supplies user intent and presentation state. IEC 61850 DataSet/RCB semantics,
/// source provenance, live reconciliation and availability decisions remain engine-owned.
/// </summary>
public sealed partial class NativeIec61850Client
{
private ArCanonical.CanonicalIedModel? _canonicalRuntimeModel;

internal ArCanonical.CanonicalIedModel? CanonicalRuntimeModel => _canonicalRuntimeModel;

private void ResetCanonicalRuntimeModel()
=> _canonicalRuntimeModel = null;

private bool TryInstallCanonicalSclRuntimeModel(
string sclXml,
string iedName,
string accessPointName,
out string error,
out IReadOnlyList<string> warnings)
{
error = string.Empty;
warnings = Array.Empty<string>();

try
{
var document = XDocument.Parse(
sclXml,
LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
var imported = ArSclEngineering.SclCanonicalImporter.Import(
document,
new ArSclEngineering.SclCanonicalImportOptions
{
IedName = iedName,
AccessPointName = accessPointName,
SourceName = "ARSAS Open SCL"
});

warnings = imported.Warnings;
if (!imported.IsSuccess || imported.Model is null)
{
error = imported.Errors.Length > 0
? string.Join(" | ", imported.Errors)
: $"Canonical SCL import failed with status {imported.Status}.";
return false;
}

_canonicalRuntimeModel = imported.Model;
return true;
}
catch (Exception ex) when (ex is System.Xml.XmlException or InvalidDataException or ArgumentException or InvalidOperationException)
{
error = $"Canonical SCL import failed: {ex.GetType().Name}: {ex.Message}";
return false;
}
}

private ArCanonical.CanonicalIedModel? ResolveCanonicalRuntimeModel(
Iec61850MonitorDevice device,
out string error)
{
error = string.Empty;

if (device.SclWorkspace?.DesignModel is not null)
{
if (_canonicalRuntimeModel is not null &&
_canonicalRuntimeModel.Source.Ingress == ArCanonical.CanonicalIngressKind.SclFile)
{
return _canonicalRuntimeModel;
}

error =
"Open SCL is active, but the source-neutral canonical SCL model is unavailable. " +
"Static reporting refused to rebuild protocol semantics from UI/live-model state.";
return null;
}

if (device.LiveDiscoveryModel is not null)
{
_canonicalRuntimeModel = ArCanonical.CanonicalLiveModelAdapter.FromLiveDiscovery(
device.LiveDiscoveryModel);
return _canonicalRuntimeModel;
}

error = "No canonical SCL or live-discovery model is available for static acquisition.";
return null;
}

private static ArCanonical.CanonicalStaticReportSelection[] BuildCanonicalStaticSelections(
IEnumerable<Iec61850MonitorPoint> points)
=> points
.Where(point => !string.IsNullOrWhiteSpace(point.IecReference))
.GroupBy(
point => $"{point.IecReference.Trim()}|{point.FunctionalConstraint.Trim()}",
StringComparer.Ordinal)
.Select(group =>
{
var point = group.First();
return new ArCanonical.CanonicalStaticReportSelection
{
Reference = point.IecReference.Trim(),
FunctionalConstraint = point.FunctionalConstraint.Trim()
};
})
.ToArray();

private static ArMms.MmsReportControlCandidate CandidateFromAvailability(
ArMms.MmsRcbAvailabilitySnapshot snapshot)
=> new()
{
Domain = snapshot.Domain,
LogicalNode = snapshot.LogicalNode,
FunctionalConstraint = snapshot.Buffered ? "BR" : "RP",
Name = snapshot.Name,
Reference = snapshot.Reference,
Buffered = snapshot.Buffered,
DataSetReference = snapshot.DataSetReference,
DataSetProbeState = snapshot.DataSetProbeState,
DataSetProbeMessage = snapshot.DataSetProbeMessage,
ReportId = snapshot.ReportId,
ConfRev = snapshot.ConfRev,
IntegrityPeriodMs = snapshot.IntegrityPeriodMs,
EnabledState = snapshot.EnabledState,
ReservationState = snapshot.ReservationState,
ReservationTimeSeconds = snapshot.ReservationTimeSeconds,
Owner = snapshot.Owner,
BufferTimeMs = snapshot.BufferTimeMs,
TriggerOptions = snapshot.TriggerOptions,
OptionalFields = snapshot.OptionalFields,
Status = $"EngineAvailability:{snapshot.Availability}",
Attributes = snapshot.Attributes.ToList()
};

private static ArMms.MmsDataSetDirectoryResult DirectoryFromAvailability(
ArMms.MmsRcbAvailabilitySnapshot snapshot)
=> new()
{
IsSuccess = snapshot.DataSetDirectorySuccess && snapshot.DataSetMembers.Count > 0,
DataSetReference = snapshot.DataSetReference,
Domain = snapshot.Domain,
DataSetMmsName = string.Empty,
IsDeletable = snapshot.DataSetIsDeletable ?? false,
Members = snapshot.DataSetMembers,
Message =
$"Engine targeted availability evidence: {snapshot.Reference} -> {snapshot.DataSetReference}, " +
$"members={snapshot.DataSetMembers.Count}, confidence={snapshot.Confidence}."
};
}
Loading