From 6d579f74b0407b2b97b7c0c8325504c39f8272dd Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 7 Oct 2026 17:00:51 +0700 Subject: [PATCH 01/21] test(interop): pin code-verified P0-P6.1 engine trial for #446 --- engines/ARIEC61850.lock.json | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/engines/ARIEC61850.lock.json b/engines/ARIEC61850.lock.json index 7c9c8aa31..9bbdef565 100644 --- a/engines/ARIEC61850.lock.json +++ b/engines/ARIEC61850.lock.json @@ -2,9 +2,9 @@ "schemaVersion": 1, "repository": "masarray/ARIEC61850", "ref": "main", - "commit": "84e9820e5a32690475960e49d5ef74e6637847fd", - "sourcePullRequest": 143, - "purpose": "Physically accepted post-v1.6.40 SCL-assisted MMS association interoperability layer. Exact tested engine head 84e9820e5a32690475960e49d5ef74e6637847fd is preserved as the runtime pin; engine PR #143 is merged on main at e5deed1d8aa11d97991695c6e390baafea7ab797 with identical tree d92676dd6d6243ec997749dd0355aa8c36ad83bd. Missing ConnectedAP association fields remain unresolved rather than fabricated; explicit valid SCL fields are immutable constraints, malformed/conflicting declarations fail closed, candidates are bounded/serial with fresh transport, and successful SCL-assisted association performs Domain/VMD validation only with no full discovery fallback. The R10 discovery/model baseline remains independently frozen by physicalTestedCommit/mergedMainCommit/mergedMainTree and previousStablePin.", + "commit": "9c5292570f55dd81be1b3a6b56f937e5ba1ed276", + "sourcePullRequest": 148, + "purpose": "CODE-VERIFIED INTEROPERABILITY TRIAL ONLY; not a production/physical promotion. Pins the exact P0-P6.1 smart interoperability integration head that passed ARIEC61850 .NET CI #759: typed RCB mutation including signed ResvTms, canonical Discovery/Open-SCL convergence, JIT configured-static activation proof, targeted RCB availability, event-driven report receive, source-aware canonical acquisition preflight, and targeted live RCB reconciliation without full discovery. All historical physical baselines and the physically accepted SCL-association ancestry below remain unchanged authority until fresh physical qualification.", "previousTrialPin": { "commit": "4467124775d8d9d76f3db194f9fbfd97144767a8", "sourcePullRequest": 134, @@ -40,5 +40,25 @@ "preservesR10DiscoveryModelBaseline": true, "fullDiscoveryFallbackForbidden": true, "cyclicMmsProcessPollingForbidden": true + }, + "smartInteroperabilityCodeTrial": { + "status": "code-verified-not-physical", + "issue": 144, + "integrationPullRequest": 148, + "featurePullRequests": [ + 145, + 146, + 147, + 149, + 150, + 151 + ], + "exactCommit": "9c5292570f55dd81be1b3a6b56f937e5ba1ed276", + "ciRun": 759, + "consumerIssue": 446, + "consumerBasePullRequest": 425, + "fullDiscoveryFallbackForbidden": true, + "vendorBranchingForbidden": true, + "physicalPromotionRequired": true } } From d051a1cad6bd690deb5242dfeac3e77d9d07e2ab Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 7 Oct 2026 17:02:24 +0700 Subject: [PATCH 02/21] feat(interop): add thin canonical acquisition consumer boundary (#446) --- ...tiveIec61850Client.CanonicalAcquisition.cs | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 Services/NativeIec61850Client.CanonicalAcquisition.cs diff --git a/Services/NativeIec61850Client.CanonicalAcquisition.cs b/Services/NativeIec61850Client.CanonicalAcquisition.cs new file mode 100644 index 000000000..1d5f128d9 --- /dev/null +++ b/Services/NativeIec61850Client.CanonicalAcquisition.cs @@ -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; + +/// +/// 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. +/// +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 warnings) + { + error = string.Empty; + warnings = Array.Empty(); + + 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 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}." + }; +} From 3087869dedb63e05c7bc25f3d82086841e1be77c Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 7 Oct 2026 17:03:00 +0700 Subject: [PATCH 03/21] refactor(scl): replace fabricated trusted RCB inventory with canonical projection (#446) --- Services/NativeIec61850Client.SclAssisted.cs | 114 +++++-------------- 1 file changed, 28 insertions(+), 86 deletions(-) diff --git a/Services/NativeIec61850Client.SclAssisted.cs b/Services/NativeIec61850Client.SclAssisted.cs index 6dde40500..051daeffb 100644 --- a/Services/NativeIec61850Client.SclAssisted.cs +++ b/Services/NativeIec61850Client.SclAssisted.cs @@ -113,6 +113,7 @@ public async Task ConnectUsingSclAsync( var initialReadDuration = TimeSpan.Zero; ResetTrustedSclOnlineAuthority(); + ResetCanonicalRuntimeModel(); await DisposeControlSessionsAsync().ConfigureAwait(false); LastErrorMessage = string.Empty; LastConnectionFailureKind = string.Empty; @@ -157,6 +158,29 @@ preparation.InitialReadDesign is null || }; } + if (!TryInstallCanonicalSclRuntimeModel( + sclXml, + iedName, + accessPointName, + out var canonicalError, + out var canonicalWarnings)) + { + LastConnectionFailureKind = "SCL_CANONICAL_IMPORT_INVALID"; + LastErrorMessage = canonicalError; + LastConnectionTechnicalSummary = canonicalError; + totalWatch.Stop(); + return new SclAssistedClientConnectResult + { + Preparation = preparation, + Warnings = preparation.Warnings + .Concat(canonicalWarnings) + .Distinct(StringComparer.Ordinal) + .ToArray(), + TotalDuration = totalWatch.Elapsed, + Message = LastErrorMessage + }; + } + try { var associationWatch = Stopwatch.StartNew(); @@ -256,7 +280,9 @@ or ArMms.InitialFcReadExecutionStatus.TimedOut StringComparer.Ordinal); _liveModel = preparation.InitialReadDesign.Model; - var reportInventory = BuildTrustedSclReportInventory(_liveModel); + var reportInventory = ArMms.MmsCanonicalReportInventoryProjection.Build( + _canonicalRuntimeModel + ?? throw new InvalidOperationException("Canonical SCL runtime model was lost after successful import.")); var dataSetDirectories = BuildModelDataSetDirectories(_liveModel, "TrustedScl"); foreach (var directory in dataSetDirectories) { @@ -344,6 +370,7 @@ or ArMms.InitialFcReadExecutionStatus.TimedOut : string.Empty; var warnings = preparation.Warnings + .Concat(canonicalWarnings) .Concat(online.AssociationResolutionNotes) .Concat(extraDomains > 0 ? new[] { $"IED exposes {extraDomains} extra online MMS domain(s); they remain evidence only and do not mutate the SCL model." } @@ -395,70 +422,6 @@ or ArMms.InitialFcReadExecutionStatus.TimedOut } } - private static ArMms.MmsReportInventory BuildTrustedSclReportInventory( - LiveIedModelDiscoveryDocument model) - { - var inventory = new ArMms.MmsReportInventory(); - foreach (var dataSet in model.DataSets) - { - var (domain, itemName) = ParseTrustedSclDataSetReference( - dataSet.Reference, - dataSet.Domain, - dataSet.LogicalNode, - dataSet.Name); - inventory.DataSets.Add(new ArMms.MmsDataSetCandidate - { - Domain = domain, - LogicalNode = dataSet.LogicalNode, - Name = dataSet.Name, - Reference = dataSet.Reference, - RawMmsName = itemName - }); - } - - foreach (var report in model.ReportControls) - { - var reference = ConcreteFirstStaticRcbReference(report.Reference, report.Indexed); - var candidate = new ArMms.MmsReportControlCandidate - { - Domain = report.Domain, - LogicalNode = report.LogicalNode, - FunctionalConstraint = report.Buffered ? "BR" : "RP", - Name = StaticRcbLeaf(reference), - Reference = reference, - Buffered = report.Buffered, - DataSetReference = report.DataSetReference, - DataSetProbeState = ArMms.MmsRcbDataSetProbeState.NotAttempted, - DataSetProbeMessage = "Trusted SCL authority; no live DataSet probe performed.", - ReportId = report.ReportId, - ConfRev = report.ConfRev, - IntegrityPeriodMs = report.IntegrityPeriodMs, - EnabledState = report.EnabledState, - ReservationState = report.ReservationState, - ReservationTimeSeconds = report.ReservationTimeSeconds, - BufferTimeMs = report.BufferTimeMs, - TriggerOptions = report.TriggerOptions, - OptionalFields = report.OptionalFields, - Status = "TrustedSclAuthority" - }; - - candidate.Attributes.AddRange(report.Buffered - ? new[] - { - "RptID", "RptEna", "DatSet", "ConfRev", "OptFlds", "BufTm", "SqNum", - "TrgOps", "IntgPd", "GI", "PurgeBuf", "EntryID", "TimeOfEntry", "ResvTms", "Owner" - } - : new[] - { - "RptID", "RptEna", "Resv", "DatSet", "ConfRev", "OptFlds", "BufTm", - "SqNum", "TrgOps", "IntgPd", "GI", "Owner" - }); - inventory.ReportControls.Add(candidate); - } - - return inventory; - } - private static IReadOnlyList BuildModelDataSetDirectories( LiveIedModelDiscoveryDocument model, string source) @@ -555,19 +518,6 @@ private static string BuildMmsItemNameFromUserReference(string reference, string : $"{logicalNode}${functionalConstraint}${dataPath}"; } - private static string ConcreteFirstStaticRcbReference(string reference, bool indexed) - { - var normalized = reference?.Trim() ?? string.Empty; - if (string.IsNullOrWhiteSpace(normalized) || !indexed) - return normalized; - - var separator = Math.Max(normalized.LastIndexOf('$'), normalized.LastIndexOf('.')); - var leaf = separator >= 0 ? normalized[(separator + 1)..] : normalized; - return leaf.Length > 0 && !char.IsDigit(leaf[^1]) - ? normalized + "01" - : normalized; - } - private static object? ConvertTrustedSclInitialValue(ArMms.MmsDataValue value) { ArgumentNullException.ThrowIfNull(value); @@ -598,14 +548,6 @@ private static string FormatTrustedSclBitString(IReadOnlyList raw) return $"bits({hex},unused={unusedBits})"; } - private static string StaticRcbLeaf(string reference) - { - var separator = Math.Max(reference.LastIndexOf('$'), reference.LastIndexOf('.')); - return separator >= 0 && separator + 1 < reference.Length - ? reference[(separator + 1)..] - : reference; - } - private static bool IsSafeTrustedSclInitialReadFc(string? functionalConstraint) => (functionalConstraint ?? string.Empty).Trim().ToUpperInvariant() is "ST" or "MX" or "SP" or "SV" or "CF" or "DC" or "EX" or "BL" or "OR" or "SR"; From fc3c31b1d17762dd65be9f191faaf550c9aa97c9 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 7 Oct 2026 17:04:03 +0700 Subject: [PATCH 04/21] refactor(reporting): consume engine canonical static smart path (#446) --- ...veIec61850Client.StaticDataSetReporting.cs | 343 +++++++----------- 1 file changed, 140 insertions(+), 203 deletions(-) diff --git a/Services/NativeIec61850Client.StaticDataSetReporting.cs b/Services/NativeIec61850Client.StaticDataSetReporting.cs index 95c052d10..d1a0433fe 100644 --- a/Services/NativeIec61850Client.StaticDataSetReporting.cs +++ b/Services/NativeIec61850Client.StaticDataSetReporting.cs @@ -32,14 +32,12 @@ public async Task BuildStaticDataSetReportPlan _deterministicStaticSubscriptions.Clear(); ResetSemanticReportProjectionContext(); - // Opened SCL is the semantic and configuration authority. A live model is authoritative - // only for online-only operation where no CID/SCD workspace is open. var projectionModel = device.SclWorkspace?.DesignModel ?? device.LiveDiscoveryModel; if (projectionModel is null) { return StaticPlanningUnavailable( points, - "Static DataSet report-only requires an opened/live SCL model with exact DataSet and RCB configuration."); + "Static DataSet report-only requires an opened/live model with exact configured DataSet membership."); } if (!_session.IsMmsInitiated) @@ -49,37 +47,21 @@ public async Task BuildStaticDataSetReportPlan $"Static DataSet report-only requires an initiated MMS association. Current state: {_session.State}."); } - SetSemanticReportProjectionAuthority(projectionModel); + var canonicalModel = ResolveCanonicalRuntimeModel(device, out var canonicalError); + if (canonicalModel is null) + return StaticPlanningUnavailable(points, canonicalError); - // P0 authority rule: when an SCL workspace exists, only ReportControls from that - // design model may authorize static acquisition. Fresh live discovery is verification - // and concrete-instance evidence; it must never introduce a peer RCB that can displace - // an explicit SCL binding such as Digital -> Buffer02. - var configurationModel = projectionModel; - var configurationAuthorityLabel = device.SclWorkspace?.DesignModel is not null - ? "opened SCL design model" - : "live discovery model (online-only)"; - - // The model that created the operator-visible DataSet inventory is also the - // positional authority for report projection. Open SCL and Smart Discovery now - // converge on this exact ordered representation. + SetSemanticReportProjectionAuthority(projectionModel); var modelDataSetDirectories = BuildModelDataSetDirectories( projectionModel, device.SclWorkspace?.DesignModel is not null ? "SclDesignModel" : "LiveDiscoveryModel"); - // Fresh report discovery is verification, not permission policy. In particular we - // deliberately do NOT require the adaptive Hybrid availability gate to classify a - // configured BRCB as Available. Some perfectly usable servers omit enough reservation - // metadata to remain Unknown. Explicit enabled/reserved/owner evidence is still used - // to avoid stealing an occupied RCB. - var discovery = await EnsureDiscoveryForReportingAsync(cancellationToken).ConfigureAwait(false); - if (discovery is null) + var selections = BuildCanonicalStaticSelections(points); + if (selections.Length == 0) { return StaticPlanningUnavailable( points, - string.IsNullOrWhiteSpace(LastErrorMessage) - ? "Fresh report discovery was unavailable." - : LastErrorMessage); + "No exact IEC 61850 signal identity was available for canonical static acquisition."); } var callerOwnedRcbReferences = _reportMonitorSessions.Values @@ -87,190 +69,153 @@ public async Task BuildStaticDataSetReportPlan .Where(reference => !string.IsNullOrWhiteSpace(reference)) .ToHashSet(StringComparer.OrdinalIgnoreCase); - var reportPlans = new List(); - var warnings = new List(); - var coveredPointKeys = new HashSet(StringComparer.OrdinalIgnoreCase); + // P6.1 engine-owned hot path: + // local canonical coverage -> targeted live RCB reconciliation -> exact-target JIT + // availability. Full discovery and application-side indexed-family fabrication are + // intentionally excluded from Static DataSet acquisition. + var smart = await _session.PrepareCanonicalStaticAcquisitionSmartAsync( + canonicalModel, + selections, + directory: null, + reconciliationOptions: new ArMms.MmsCanonicalStaticLiveReconciliationOptions + { + MaxDomains = 16, + MaxVariableNamesPerDomain = 20000, + MaxNameListPages = 64, + MaxConcurrentDomains = 4, + UnknownPeerMaxConcurrentDomains = 2, + MaxReportControlCandidates = 64 + }, + acquisitionOptions: new ArMms.MmsCanonicalStaticAcquisitionProbeOptions + { + MaxExactTargets = 64, + ReadDataSetDirectories = true, + CallerOwnedRcbReferences = callerOwnedRcbReferences + }, + cancellationToken) + .ConfigureAwait(false); - var selectedByDataSet = points - .Where(point => !string.IsNullOrWhiteSpace(point.DataSetReference)) - .GroupBy(point => NormalizeStaticReference(point.DataSetReference), StringComparer.OrdinalIgnoreCase) - .Where(group => !string.IsNullOrWhiteSpace(group.Key)) - .OrderBy(group => group.Key, StringComparer.OrdinalIgnoreCase) - .ToArray(); + var warnings = smart.Reconciliation.Warnings + .Concat(smart.Acquisition.Warnings) + .Distinct(StringComparer.Ordinal) + .ToList(); - foreach (var dataSetGroup in selectedByDataSet) + var availability = smart.Acquisition.Availability; + if (availability is null) { - cancellationToken.ThrowIfCancellationRequested(); - - var dataSetReference = dataSetGroup.First().DataSetReference.Trim(); - var configuredReports = configurationModel.ReportControls - .Where(report => SameStaticReference(report.DataSetReference, dataSetGroup.Key)) - .GroupBy( - report => $"{NormalizeStaticReference(report.Reference)}|{NormalizeStaticReference(report.DataSetReference)}", - StringComparer.OrdinalIgnoreCase) - .Select(group => group.First()) - .OrderByDescending(report => report.Buffered) - .ThenBy(report => report.Reference, StringComparer.OrdinalIgnoreCase) - .ToArray(); + return StaticPlanningUnavailable( + points, + $"{smart.Summary} No exact live RCB availability evidence was produced."); + } - if (configuredReports.Length == 0) - { - warnings.Add( - $"{dataSetReference}: no configured BRCB/URCB in the authoritative {configurationAuthorityLabel}; {dataSetGroup.Count()} selected point(s) remain explicitly unavailable. No MMS process polling was substituted."); - continue; - } + var reportPlans = new List(); + var coveredPointKeys = new HashSet(StringComparer.OrdinalIgnoreCase); - // Evaluate every authoritative configured RCB instead of selecting only the first. - // If several SCL ReportControls legitimately reference the same DataSet, one - // missing/occupied RCB must not hide another configured option. A configured family - // may resolve only to decimal indexed instances of that same literal family; - // arbitrary same-DataSet substitution remains forbidden. - var matchedLiveCandidates = configuredReports - .SelectMany(configured => discovery.ReportInventory.ReportControls - .Select(candidate => new - { - Configured = configured, - Candidate = candidate, - Rank = Iec61850StaticRcbReferenceMatcher.MatchRank( - configured.Reference, - candidate.Reference) - }) - .Where(item => item.Rank != int.MaxValue) - .Where(item => - string.IsNullOrWhiteSpace(item.Candidate.DataSetReference) || - SameStaticReference(item.Candidate.DataSetReference, dataSetReference))) - .OrderBy(item => item.Rank) - .ThenByDescending(item => item.Configured.Buffered) - .ThenBy(item => item.Configured.Reference, StringComparer.OrdinalIgnoreCase) - .ThenBy(item => item.Candidate.Reference, StringComparer.OrdinalIgnoreCase) - .ToArray(); + foreach (var segment in smart.Coverage.Segments) + { + cancellationToken.ThrowIfCancellationRequested(); - // Use ARIEC's operational evaluator so Owner is part of the same occupancy - // decision as RptEna/Resv/ResvTms. Directory evidence is intentionally omitted at - // this stage: Unknown is still eligible and the exact DataSet directory is verified - // before the plan is armed. If a currently active session already owns the RCB, - // preserve that fact as UsedByCaller instead of misclassifying it as foreign use. - var evaluatedLiveCandidates = matchedLiveCandidates - .Select(item => new + var targetSegment = smart.Acquisition.Plan.TargetResolution.Segments + .SingleOrDefault(item => + SameStaticReference(item.DataSetReference, segment.DataSetReference)); + var exactTargets = targetSegment?.ExactLiveReportControlReferences + .Select(NormalizeStaticReference) + .Where(reference => reference.Length > 0) + .ToHashSet(StringComparer.OrdinalIgnoreCase) + ?? new HashSet(StringComparer.OrdinalIgnoreCase); + + var eligible = availability.ReportControls + .Where(snapshot => + exactTargets.Contains(NormalizeStaticReference(snapshot.Reference))) + .Select(snapshot => new { - item.Configured, - item.Candidate, - item.Rank, - Availability = ArMms.MmsRcbAvailabilityEvaluator.Evaluate( - item.Candidate, - dataSetDirectory: null, - callerOwned: callerOwnedRcbReferences.Contains( - NormalizeStaticReference(item.Candidate.Reference))) + Snapshot = snapshot, + Eligibility = ArMms.MmsConfiguredStaticRcbEligibilityPolicy.Evaluate( + snapshot, + allowCallerOwned: true, + allowReducedMissingReservationEvidence: true) }) + .Where(item => item.Eligibility.IsEligible) + .OrderBy(item => item.Eligibility.Kind switch + { + ArMms.MmsConfiguredStaticRcbEligibilityKind.CallerOwned => 0, + ArMms.MmsConfiguredStaticRcbEligibilityKind.ExplicitFree => 1, + ArMms.MmsConfiguredStaticRcbEligibilityKind.ReducedMissingReservationEvidence => 2, + _ => 9 + }) + .ThenByDescending(item => item.Snapshot.Buffered) + .ThenBy(item => item.Snapshot.Reference, StringComparer.OrdinalIgnoreCase) .ToArray(); - // An indexed family can expose several concrete RCBs. Literal instance order is - // not an activation policy: Buffer01 may already be enabled/reserved/owned while - // Buffer02 is the usable instance. Reject only explicit operational blockers; - // Unknown remains eligible because some relays omit reservation metadata. Among - // equally matched candidates prefer exact identity, caller-owned/known-safe state, - // BRCB, explicitly disabled state, and finally stable literal order. - var liveCandidates = evaluatedLiveCandidates - .Where(item => StaticRcbAvailabilityRank(item.Availability.Availability) != int.MaxValue) - .Where(item => !ArMms.MmsReportSubscriptionPlanner.IsExplicitlyEnabled(item.Candidate)) - .Where(item => !ArMms.MmsReportSubscriptionPlanner.IsReservedByOtherClient(item.Candidate)) - .OrderBy(item => item.Rank) - .ThenBy(item => StaticRcbAvailabilityRank(item.Availability.Availability)) - .ThenByDescending(item => item.Configured.Buffered) - .ThenByDescending(item => ArMms.MmsReportSubscriptionPlanner.IsExplicitlyDisabled(item.Candidate)) - .ThenBy(item => item.Configured.Reference, StringComparer.OrdinalIgnoreCase) - .ThenBy(item => item.Candidate.Reference, StringComparer.OrdinalIgnoreCase) - .ToArray(); - - if (liveCandidates.Length == 0) + if (eligible.Length == 0) { - if (evaluatedLiveCandidates.Length > 0) - { - var occupied = string.Join( - ", ", - evaluatedLiveCandidates.Take(8).Select(item => - $"{item.Configured.Reference}->{item.Candidate.Reference}[availability={item.Availability.Availability}, RptEna={item.Candidate.EnabledState}, Resv={item.Candidate.ReservationState}, ResvTms={item.Candidate.ReservationTimeSeconds}, Owner={item.Candidate.Owner}]")); - warnings.Add( - $"{dataSetReference}: exact/indexed-family RCB objects were found for authoritative configuration, but every concrete instance was explicitly unavailable/in-use ({occupied}). Static mode will not steal an occupied RCB and did not poll process values."); - continue; - } - - var configuredNames = string.Join(", ", configuredReports - .Select(report => report.Reference) - .Where(reference => !string.IsNullOrWhiteSpace(reference)) - .Take(8)); - var sameDataSet = discovery.ReportInventory.ReportControls - .Where(candidate => - !string.IsNullOrWhiteSpace(candidate.DataSetReference) && - SameStaticReference(candidate.DataSetReference, dataSetReference)) - .Select(candidate => candidate.Reference) - .Where(reference => !string.IsNullOrWhiteSpace(reference)) - .OrderBy(reference => reference, StringComparer.OrdinalIgnoreCase) + var rejected = availability.ReportControls + .Where(snapshot => + exactTargets.Contains(NormalizeStaticReference(snapshot.Reference))) + .Select(snapshot => + { + var evaluation = ArMms.MmsConfiguredStaticRcbEligibilityPolicy.Evaluate( + snapshot, + allowCallerOwned: true, + allowReducedMissingReservationEvidence: true); + return $"{snapshot.Reference}[{snapshot.Availability}/{snapshot.Confidence}: {evaluation.Reason}]"; + }) .Take(8) .ToArray(); - var observed = sameDataSet.Length == 0 - ? "none" - : string.Join(", ", sameDataSet); warnings.Add( - $"{dataSetReference}: authoritative configured RCB(s) [{configuredNames}] had no exact/indexed-family live instance. Same-DataSet live RCBs: {observed}. Static mode refused arbitrary substitution and did not poll process values."); + $"{segment.DataSetReference}: engine resolved configured static coverage but no exact live RCB is eligible. " + + (rejected.Length == 0 ? "No exact live target evidence." : string.Join(", ", rejected))); continue; } - var selected = liveCandidates[0]; - var configured = selected.Configured; - var liveSource = selected.Candidate; - var liveAvailability = selected.Availability; - var liveRcb = CloneReportControlForPlanning(liveSource); + var selected = eligible[0]; + var liveRcb = CandidateFromAvailability(selected.Snapshot); + var configured = segment.ReportControls + .Where(report => report.Buffered == selected.Snapshot.Buffered) + .OrderBy(report => report.Reference, StringComparer.Ordinal) + .FirstOrDefault() + ?? segment.ReportControls.FirstOrDefault(); - if (configuredReports.Length > 1 || liveCandidates.Length > 1) + if (configured is null) { warnings.Add( - $"{dataSetReference}: evaluated {configuredReports.Length} authoritative configured RCB(s) and {liveCandidates.Length} non-occupied exact/indexed live match(es); selected {configured.Reference} -> {liveSource.Reference} ({liveAvailability.Availability}). {configurationAuthorityLabel} remained authoritative."); - } - - if (!string.IsNullOrWhiteSpace(liveRcb.DataSetReference) && - !SameStaticReference(liveRcb.DataSetReference, dataSetReference)) - { - warnings.Add( - $"{configured.Reference}: authoritative configuration binds {dataSetReference}, but live DatSet reports {liveRcb.DataSetReference}. Static mode refused the mismatch instead of guessing or polling."); + $"{segment.DataSetReference}: canonical coverage contained no configured ReportControl metadata; target was not armed."); continue; } - // Missing live DatSet text is not treated as a reason to discard correct - // authoritative configuration. The exact DataSet directory below is still required - // and is the ordered mapping authority for InformationReport values. - if (string.IsNullOrWhiteSpace(liveRcb.DataSetReference)) - liveRcb.DataSetReference = dataSetReference; - var modelDirectory = modelDataSetDirectories.SingleOrDefault(result => - result.IsSuccess && SameStaticReference(result.DataSetReference, dataSetReference)); + result.IsSuccess && + SameStaticReference(result.DataSetReference, segment.DataSetReference)); if (modelDirectory is null || modelDirectory.Members.Count == 0) { warnings.Add( - $"{dataSetReference}: canonical model has no ordered DataSet member list. RCB was not armed because report-index mapping would be unsafe; MMS process polling remains disabled."); + $"{segment.DataSetReference}: canonical model has no ordered DataSet members. Unsafe positional projection was refused."); continue; } - // Golden Smart Discovery already captured the live DataSet directory in the - // authoritative single-flight. Reuse that evidence instead of performing a - // second directory request after the model is built. - var liveDirectory = discovery.DataSetDirectories.SingleOrDefault(result => - result.IsSuccess && SameStaticReference(result.DataSetReference, dataSetReference)); - if (liveDirectory is null || liveDirectory.Members.Count == 0) + var liveDirectory = DirectoryFromAvailability(selected.Snapshot); + if (!liveDirectory.IsSuccess || liveDirectory.Members.Count == 0) { warnings.Add( - $"{dataSetReference}: discovery authority has no ordered live DataSet directory. RCB was not armed; re-scan is required and MMS process polling remains disabled."); + $"{segment.DataSetReference}: targeted live availability did not prove a populated DataSet directory. RCB was not armed."); continue; } if (!TryVerifyStaticDataSetMemberOrder(modelDirectory, liveDirectory, out var directoryMismatch)) { warnings.Add( - $"{dataSetReference}: canonical/live DataSet member order mismatch ({directoryMismatch}). RCB was not armed; unsafe positional projection and MMS process polling were both refused."); + $"{segment.DataSetReference}: canonical/live DataSet member order mismatch ({directoryMismatch}). RCB was not armed."); continue; } - var bindings = dataSetGroup + var bindings = points + .Where(point => + SameStaticReference(point.DataSetReference, segment.DataSetReference) || + segment.SelectedSignalReferences.Any(reference => + string.Equals( + NormalizeStaticReference(reference), + NormalizeStaticReference(point.IecReference), + StringComparison.OrdinalIgnoreCase))) .GroupBy(point => point.PointKey, StringComparer.OrdinalIgnoreCase) .Select(group => group.First()) .OrderBy(point => point.IecReference, StringComparer.OrdinalIgnoreCase) @@ -278,45 +223,36 @@ public async Task BuildStaticDataSetReportPlan if (bindings.Count == 0) continue; - var concreteReportReference = string.IsNullOrWhiteSpace(liveRcb.Reference) - ? configured.Reference - : liveRcb.Reference; var plan = new ReportControlPlan { RelayId = device.DeviceId, RelayName = device.Name, RelayIpAddress = device.IpAddress, IedName = device.Name, - ReportControlReference = concreteReportReference, - DataSetReference = dataSetReference, - Mode = "Static DataSet • deterministic configured RCB", + ReportControlReference = selected.Snapshot.Reference, + DataSetReference = segment.DataSetReference, + Mode = "Static DataSet • engine canonical configured RCB", AllowDynamicDataSetWrites = false, - Buffered = configured.Buffered, - ReportId = liveRcb.ReportId, - IntegrityPeriodMs = ParseStaticInteger(liveRcb.IntegrityPeriodMs), - TriggerOptions = liveRcb.TriggerOptions, - OptionalFields = liveRcb.OptionalFields, - Status = "Deterministic static report planned", + Buffered = selected.Snapshot.Buffered, + ReportId = selected.Snapshot.ReportId, + IntegrityPeriodMs = ParseStaticInteger(selected.Snapshot.IntegrityPeriodMs), + TriggerOptions = selected.Snapshot.TriggerOptions, + OptionalFields = selected.Snapshot.OptionalFields, + Status = "Engine canonical static report planned", IsEngineAuthoritative = true, - EngineAcquisitionKind = configured.Buffered ? "StaticBrcb" : "StaticUrcb", + EngineAcquisitionKind = selected.Snapshot.Buffered ? "StaticBrcb" : "StaticUrcb", Bindings = bindings }; - var subscriptionWarnings = new List(); - if (!Iec61850StaticRcbReferenceMatcher.IsExact(configured.Reference, concreteReportReference)) - { - subscriptionWarnings.Add( - $"Configured ReportControl family {configured.Reference} resolved to concrete live indexed instance {concreteReportReference}."); - } - if (liveAvailability.Availability == ArMms.MmsRcbOperationalAvailability.Unknown) + var subscriptionWarnings = new List { - subscriptionWarnings.Add( - $"Live RCB availability for {concreteReportReference} remains Unknown ({liveAvailability.Reason}). Static mode permits this because identity/configuration are authoritative and no explicit in-use evidence was observed."); - } - if (string.IsNullOrWhiteSpace(liveSource.DataSetReference)) + $"Engine eligibility={selected.Eligibility.Kind}: {selected.Eligibility.Reason}", + $"Targeted reconciliation={smart.Reconciliation.Status}; no full report discovery was used." + }; + if (selected.Eligibility.UsesReducedEvidence) { subscriptionWarnings.Add( - "Live RCB DatSet text was not returned; exact authoritative RCB->DataSet configuration plus the successfully read live DataSet directory are the deterministic authority."); + "Reservation metadata is missing/reduced. Activation remains fail-closed and must prove RptEna/readback; report traffic is post-activation proof."); } var subscription = new ArMms.MmsReportSubscriptionPlan @@ -324,16 +260,17 @@ public async Task BuildStaticDataSetReportPlan Mode = ArMms.MmsReportSubscriptionPlanMode.StaticDataSet, Status = ArMms.MmsReportSubscriptionPlanStatus.ReadyRequiresWrite, ReportControl = liveRcb, - DataSetReference = dataSetReference, + DataSetReference = segment.DataSetReference, Members = modelDirectory.Members, DynamicPoints = Array.Empty(), Steps = new[] { - $"Verify authoritative configured RCB {configured.Reference} as live object {concreteReportReference}.", - $"Use canonical ordered DataSet members {dataSetReference} ({modelDirectory.Members.Count} members), verified against the authoritative discovery evidence.", - "Install InformationReport receiver before enabling the RCB.", - "Use client-compatible BRCB reservation when ResvTms is exposed, enable RptEna, then request GI after receiver registration.", - "Map report values by ordered DataSet member index; never substitute cyclic MMS process reads." + $"Use canonical configured static coverage for {segment.DataSetReference}.", + $"Use exact live RCB {selected.Snapshot.Reference} proven by engine reconciliation.", + $"Use {modelDirectory.Members.Count} ordered canonical DataSet member(s), verified against targeted live directory evidence.", + "Install InformationReport receiver before RptEna mutation.", + "Use engine configured-static transactional activation and optional GI.", + "Treat traffic as post-activation evidence; never enable cyclic MMS process polling for this static segment." }, Warnings = subscriptionWarnings }; @@ -355,11 +292,11 @@ public async Task BuildStaticDataSetReportPlan return new NativeHybridReportPlanningResult { IsAuthoritative = true, - Authority = "Deterministic Static DataSet configured-RCB path", + Authority = "ARIEC canonical static acquisition", Status = reportPlans.Count > 0 ? "StaticReportReady" : "StaticReportingUnavailable", Summary = reportPlans.Count > 0 - ? $"Deterministic Static DataSet path prepared {reportPlans.Count} configured RCB plan(s), covering {coveredPointKeys.Count}/{points.Count} selected point(s). Hybrid planning, dynamic DataSet writes and cyclic MMS process polling were bypassed." - : $"No configured Static DataSet RCB could be armed safely for {points.Count} selected point(s). Hybrid planning and cyclic MMS process polling were not used.", + ? $"Engine canonical static path prepared {reportPlans.Count} RCB plan(s), covering {coveredPointKeys.Count}/{points.Count} selected point(s). {smart.Summary}" + : $"No exact configured static RCB could be armed safely for {points.Count} selected point(s). {smart.Summary}", ReportPlans = reportPlans, PollingPointKeys = Array.Empty(), UncoveredPointKeys = uncovered, From b2cfc501a9b8bd6fc47f19cea3ee6442236c6910 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 7 Oct 2026 17:04:30 +0700 Subject: [PATCH 05/21] refactor(reporting): activate static plans without rediscovery (#446) --- ...veIec61850Client.StaticDataSetReporting.cs | 43 +++++++------------ 1 file changed, 15 insertions(+), 28 deletions(-) diff --git a/Services/NativeIec61850Client.StaticDataSetReporting.cs b/Services/NativeIec61850Client.StaticDataSetReporting.cs index d1a0433fe..cd540db7e 100644 --- a/Services/NativeIec61850Client.StaticDataSetReporting.cs +++ b/Services/NativeIec61850Client.StaticDataSetReporting.cs @@ -363,52 +363,39 @@ public async Task StartStaticDataSetReportMonito }; } - var discovery = await EnsureDiscoveryForReportingAsync(cancellationToken).ConfigureAwait(false); - if (discovery is null) - { - return new NativeReportMonitorStartResult - { - IsSuccess = false, - PlanId = plan.PlanId, - Message = string.IsNullOrWhiteSpace(LastErrorMessage) ? "Fresh report discovery unavailable." : LastErrorMessage, - SubscriptionSummary = subscription.Summary, - MemberCount = subscription.Members.Count, - FailureReason = "FreshReportDiscoveryUnavailable" - }; - } - var coveredReferences = ExtractSubscriptionMemberReferences(subscription.Members); - var attempt = await RunMmsOperationAsync( - () => _session.StartPersistentReportMonitorClientCompatibleAsync( + + // The plan already contains engine-proven exact live RCB/DataSet evidence. + // Do not repeat broad report discovery here. Configured-static activation owns + // the minimal transactional lifecycle: receiver first, JIT RCB snapshot, + // direct RptEna primary path, bounded reservation compatibility fallback, + // readback proof, then optional GI. + var start = await _session.StartConfiguredStaticReportMonitorAsync( subscription, triggerGeneralInterrogation: true, - deleteDynamicDataSetOnStop: false, - discovery.IedDirectory, - cancellationToken), - cancellationToken).ConfigureAwait(false); - var start = attempt.StartResult; + cancellationToken) + .ConfigureAwait(false); var warnings = start.Warnings .Concat(subscription.Warnings) - .Concat(attempt.CleanupWarnings) .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); - if (!attempt.IsSuccess || start.Session is null) + if (!start.IsSuccess || start.Session is null) { return new NativeReportMonitorStartResult { IsSuccess = false, PlanId = plan.PlanId, - Message = $"Deterministic Static DataSet activation failed for {plan.DisplayReference}: {start.Message}", + Message = $"Engine configured-static activation failed for {plan.DisplayReference}: {start.Message}", SubscriptionSummary = subscription.Summary, MemberCount = subscription.Members.Count, WriteStepCount = start.WriteSteps.Count, UsedDynamicDataSet = false, DynamicAttempted = false, DynamicAttemptState = "NotApplicable", - FailureReason = attempt.FailureReason.ToString(), - CleanupAttempted = attempt.CleanupAttempted, - CleanupSucceeded = attempt.CleanupSucceeded, + FailureReason = "ConfiguredStaticActivationFailed", + CleanupAttempted = false, + CleanupSucceeded = false, ReportControlReference = plan.ReportControlReference, DataSetReference = plan.DataSetReference, CoveredReferences = coveredReferences, @@ -428,7 +415,7 @@ public async Task StartStaticDataSetReportMonito { IsSuccess = true, PlanId = plan.PlanId, - Message = $"Deterministic Static DataSet {plan.EngineAcquisitionKind} active. {start.Message}", + Message = $"Engine configured-static {plan.EngineAcquisitionKind} active; traffic proof remains asynchronous. {start.Message}", SubscriptionSummary = subscription.Summary, MemberCount = subscription.Members.Count, WriteStepCount = start.WriteSteps.Count, From 951bc872c0a3cab8d289fe2700e038298c099e76 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 7 Oct 2026 17:05:48 +0700 Subject: [PATCH 06/21] test(interop): lock thin ARSAS canonical static consumer contract (#446) --- ...veryStaticWorkflowParityRegressionTests.cs | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/tests/ARSAS.Tests/DiscoveryStaticWorkflowParityRegressionTests.cs b/tests/ARSAS.Tests/DiscoveryStaticWorkflowParityRegressionTests.cs index dcbe69b00..1e9634ab0 100644 --- a/tests/ARSAS.Tests/DiscoveryStaticWorkflowParityRegressionTests.cs +++ b/tests/ARSAS.Tests/DiscoveryStaticWorkflowParityRegressionTests.cs @@ -161,14 +161,16 @@ public void StaticReporting_ReusesCompletedDiscoveryAuthority_WithoutRediscovery } [Fact] - public void StaticReporting_UsesCanonicalMemberOrder_WithoutSecondDirectoryRead() + public void StaticReporting_UsesEngineCanonicalHotPath_AndCanonicalMemberOrder() { var source = Read("Services/NativeIec61850Client.StaticDataSetReporting.cs"); - Assert.Contains("BuildModelDataSetDirectories", source, StringComparison.Ordinal); - Assert.Contains("discovery.DataSetDirectories.SingleOrDefault", source, StringComparison.Ordinal); + Assert.Contains("PrepareCanonicalStaticAcquisitionSmartAsync", source, StringComparison.Ordinal); + Assert.Contains("MmsConfiguredStaticRcbEligibilityPolicy.Evaluate", source, StringComparison.Ordinal); + Assert.Contains("DirectoryFromAvailability", source, StringComparison.Ordinal); Assert.Contains("TryVerifyStaticDataSetMemberOrder", source, StringComparison.Ordinal); Assert.Contains("Members = modelDirectory.Members", source, StringComparison.Ordinal); + Assert.DoesNotContain("discovery.DataSetDirectories.SingleOrDefault", source, StringComparison.Ordinal); Assert.DoesNotContain("GetDataSetDirectoriesAsync", source, StringComparison.Ordinal); Assert.DoesNotContain("Members = liveDirectory.Members", source, StringComparison.Ordinal); Assert.Contains("PollingPointKeys = Array.Empty()", source, StringComparison.Ordinal); @@ -185,6 +187,22 @@ public void OpenedModelAndDiscoveredModel_ShareTheSameDataSetProjectionBuilder() Assert.Contains("Source = source", source, StringComparison.Ordinal); } + [Fact] + public void DiscoveryAndOpenScl_StaticPlanning_EnterOneEngineAcquisitionContract() + { + var canonical = Read("Services/NativeIec61850Client.CanonicalAcquisition.cs"); + var staticReporting = Read("Services/NativeIec61850Client.StaticDataSetReporting.cs"); + + Assert.Contains("CanonicalLiveModelAdapter.FromLiveDiscovery", canonical, StringComparison.Ordinal); + Assert.Contains("CanonicalIngressKind.SclFile", canonical, StringComparison.Ordinal); + Assert.Contains("BuildCanonicalStaticSelections", canonical, StringComparison.Ordinal); + Assert.Contains("PrepareCanonicalStaticAcquisitionSmartAsync", staticReporting, StringComparison.Ordinal); + Assert.DoesNotContain("Iec61850StaticRcbReferenceMatcher.MatchRank", staticReporting, StringComparison.Ordinal); + Assert.DoesNotContain("EnsureDiscoveryForReportingAsync(cancellationToken)", staticReporting, StringComparison.Ordinal); + Assert.Contains("StartConfiguredStaticReportMonitorAsync", staticReporting, StringComparison.Ordinal); + Assert.DoesNotContain("StartPersistentReportMonitorClientCompatibleAsync", staticReporting, StringComparison.Ordinal); + } + [Fact] public void ReportProjection_UsesSameModelAuthorityPrecedenceAsPlanning() { @@ -225,11 +243,12 @@ public void OpenScl_Uses_EngineOwned_Bounded_Association_Resolution_Without_Disc Assert.DoesNotContain("errors.AddRange(exactAssociation.Errors)", preparation, StringComparison.Ordinal); var client = Read("Services/NativeIec61850Client.SclAssisted.cs"); + var canonical = Read("Services/NativeIec61850Client.CanonicalAcquisition.cs"); var start = client.IndexOf( "public async Task ConnectUsingSclAsync", StringComparison.Ordinal); var end = client.IndexOf( - "private static ArMms.MmsReportInventory BuildTrustedSclReportInventory", + "private static IReadOnlyList BuildModelDataSetDirectories", start, StringComparison.Ordinal); @@ -238,10 +257,15 @@ public void OpenScl_Uses_EngineOwned_Bounded_Association_Resolution_Without_Disc Assert.Contains("preparation.AssociationResolution", connectFlow, StringComparison.Ordinal); Assert.Contains("_session.ConnectSclAssistedAsync", connectFlow, StringComparison.Ordinal); + Assert.Contains("TryInstallCanonicalSclRuntimeModel", connectFlow, StringComparison.Ordinal); + Assert.Contains("MmsCanonicalReportInventoryProjection.Build", connectFlow, StringComparison.Ordinal); Assert.Contains("online.SelectedAssociationCandidateName", connectFlow, StringComparison.Ordinal); Assert.Contains("online.AssociationAttemptCount", connectFlow, StringComparison.Ordinal); Assert.DoesNotContain(".DiscoverAsync(", connectFlow, StringComparison.Ordinal); Assert.DoesNotContain("DiscoverSignals", connectFlow, StringComparison.Ordinal); + Assert.DoesNotContain("BuildTrustedSclReportInventory", client, StringComparison.Ordinal); + Assert.DoesNotContain("ConcreteFirstStaticRcbReference", client, StringComparison.Ordinal); + Assert.Contains("SclCanonicalImporter.Import", canonical, StringComparison.Ordinal); } private static string Read(string relativePath) From 1c358488365b16ff7a3eb1946c6b45c7ff7e69a5 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 7 Oct 2026 17:09:42 +0700 Subject: [PATCH 07/21] test(interop): update static path contracts to engine-owned P7 flow (#446) --- ...ministicStaticReportPathRegressionTests.cs | 78 +++++++++---------- 1 file changed, 37 insertions(+), 41 deletions(-) diff --git a/tests/ARSAS.Tests/DeterministicStaticReportPathRegressionTests.cs b/tests/ARSAS.Tests/DeterministicStaticReportPathRegressionTests.cs index 7d77b7ce9..0bb3f0262 100644 --- a/tests/ARSAS.Tests/DeterministicStaticReportPathRegressionTests.cs +++ b/tests/ARSAS.Tests/DeterministicStaticReportPathRegressionTests.cs @@ -3,29 +3,28 @@ namespace ARSAS.Tests; public sealed class DeterministicStaticReportPathRegressionTests { [Fact] - public void StaticPath_BypassesAdaptiveHybridPlannerAndPolling() + public void StaticPath_UsesEngineCanonicalHotPath_AndNeverEnablesPolling() { var source = Read("Services/NativeIec61850Client.StaticDataSetReporting.cs"); - Assert.Contains("Deterministic Static DataSet configured-RCB path", source, StringComparison.Ordinal); - Assert.Contains("var configurationModel = projectionModel", source, StringComparison.Ordinal); - Assert.Contains("configurationModel.ReportControls", source, StringComparison.Ordinal); - Assert.Contains("discovery.ReportInventory.ReportControls", source, StringComparison.Ordinal); + Assert.Contains("PrepareCanonicalStaticAcquisitionSmartAsync", source, StringComparison.Ordinal); + Assert.Contains("MmsConfiguredStaticRcbEligibilityPolicy.Evaluate", source, StringComparison.Ordinal); Assert.Contains("BuildModelDataSetDirectories", source, StringComparison.Ordinal); - Assert.Contains("discovery.DataSetDirectories", source, StringComparison.Ordinal); + Assert.Contains("DirectoryFromAvailability", source, StringComparison.Ordinal); Assert.Contains("TryVerifyStaticDataSetMemberOrder", source, StringComparison.Ordinal); - Assert.DoesNotContain("GetDataSetDirectoriesAsync", source, StringComparison.Ordinal); Assert.Contains("MmsReportSubscriptionPlanStatus.ReadyRequiresWrite", source, StringComparison.Ordinal); + Assert.Contains("StartConfiguredStaticReportMonitorAsync", source, StringComparison.Ordinal); Assert.Contains("triggerGeneralInterrogation: true", source, StringComparison.Ordinal); - Assert.Contains("deleteDynamicDataSetOnStop: false", source, StringComparison.Ordinal); Assert.Contains("PollingPointKeys = Array.Empty()", source, StringComparison.Ordinal); Assert.Contains("PollingFallbackSignalCount = 0", source, StringComparison.Ordinal); - Assert.DoesNotContain("configurationModels", source, StringComparison.Ordinal); + Assert.DoesNotContain("EnsureDiscoveryForReportingAsync(cancellationToken)", source, StringComparison.Ordinal); + Assert.DoesNotContain("Iec61850StaticRcbReferenceMatcher.MatchRank", source, StringComparison.Ordinal); Assert.DoesNotContain("MmsCapabilityAwareHybridReportAcquisitionPlanner", source, StringComparison.Ordinal); Assert.DoesNotContain("BuildDynamicPlan", source, StringComparison.Ordinal); Assert.DoesNotContain("DefineNamedVariableList", source, StringComparison.Ordinal); Assert.DoesNotContain("ReadValueAsync", source, StringComparison.Ordinal); + Assert.DoesNotContain("StartPersistentReportMonitorClientCompatibleAsync", source, StringComparison.Ordinal); } [Fact] @@ -47,67 +46,64 @@ public void TrustedSclStaticPath_RequestsOneShotGi_WhileSafeTrialRemainsReadOnly } [Fact] - public void StaticPath_RequiresConfiguredRcbFamilyAndCanonicalOrderVerifiedByDiscoverySnapshot() + public void StaticPath_RequiresEngineProvenExactRcb_AndCanonicalLiveMemberOrder() { var source = Read("Services/NativeIec61850Client.StaticDataSetReporting.cs"); - Assert.Contains("SameStaticReference(report.DataSetReference, dataSetGroup.Key)", source, StringComparison.Ordinal); - Assert.Contains("Iec61850StaticRcbReferenceMatcher.MatchRank", source, StringComparison.Ordinal); - Assert.Contains("arbitrary same-DataSet", source, StringComparison.OrdinalIgnoreCase); - Assert.Contains("canonical model has no ordered DataSet member list", source, StringComparison.Ordinal); - Assert.Contains("discovery authority has no ordered live DataSet directory", source, StringComparison.Ordinal); + Assert.Contains("smart.Acquisition.Plan.TargetResolution.Segments", source, StringComparison.Ordinal); + Assert.Contains("ExactLiveReportControlReferences", source, StringComparison.Ordinal); + Assert.Contains("MmsConfiguredStaticRcbEligibilityPolicy.Evaluate", source, StringComparison.Ordinal); + Assert.Contains("canonical model has no ordered DataSet members", source, StringComparison.Ordinal); + Assert.Contains("targeted live availability did not prove a populated DataSet directory", source, StringComparison.Ordinal); Assert.Contains("canonical/live DataSet member order mismatch", source, StringComparison.Ordinal); Assert.Contains("Members = modelDirectory.Members", source, StringComparison.Ordinal); Assert.DoesNotContain("Members = liveDirectory.Members", source, StringComparison.Ordinal); - Assert.Contains("No MMS process polling was substituted", source, StringComparison.Ordinal); - Assert.Contains("authoritative configuration binds", source, StringComparison.Ordinal); - Assert.Contains("live DatSet reports", source, StringComparison.Ordinal); - Assert.Contains("ReportControlReference = concreteReportReference", source, StringComparison.Ordinal); + Assert.Contains("ReportControlReference = selected.Snapshot.Reference", source, StringComparison.Ordinal); + Assert.DoesNotContain("Iec61850StaticRcbReferenceMatcher.MatchRank", source, StringComparison.Ordinal); } [Fact] - public void StaticPath_DoesNotIssueSecondDataSetDirectoryRead() + public void StaticPath_ConsumesTargetedAvailabilityDirectoryEvidence_WithoutRediscovery() { var source = Read("Services/NativeIec61850Client.StaticDataSetReporting.cs"); - Assert.Contains("discovery.DataSetDirectories.SingleOrDefault", source, StringComparison.Ordinal); + Assert.Contains("DirectoryFromAvailability(selected.Snapshot)", source, StringComparison.Ordinal); Assert.Contains("Members = modelDirectory.Members", source, StringComparison.Ordinal); Assert.DoesNotContain("GetDataSetDirectoriesAsync", source, StringComparison.Ordinal); + Assert.DoesNotContain("EnsureDiscoveryForReportingAsync(cancellationToken)", source, StringComparison.Ordinal); Assert.DoesNotContain("Members = liveDirectory.Members", source, StringComparison.Ordinal); } [Fact] - public void StaticPath_SclAuthorityCannotBeDisplacedByLivePeerRcb() + public void StaticPath_SclAuthorityConvergesThroughCanonicalCoverage_NotLivePeerSubstitution() { var source = Read("Services/NativeIec61850Client.StaticDataSetReporting.cs"); + var canonical = Read("Services/NativeIec61850Client.CanonicalAcquisition.cs"); Assert.Contains("device.SclWorkspace?.DesignModel ?? device.LiveDiscoveryModel", source, StringComparison.Ordinal); - Assert.Contains("only ReportControls from that", source, StringComparison.Ordinal); - Assert.Contains("design model may authorize static acquisition", source, StringComparison.Ordinal); - Assert.Contains("var configurationModel = projectionModel", source, StringComparison.Ordinal); - Assert.Contains("SelectMany(configured => discovery.ReportInventory.ReportControls", source, StringComparison.Ordinal); - Assert.DoesNotContain("SelectMany(configurationModel => configurationModel.ReportControls)", source, StringComparison.Ordinal); - Assert.DoesNotContain("var configured = configuredReports[0]", source, StringComparison.Ordinal); - Assert.Contains("remained authoritative", source, StringComparison.Ordinal); + Assert.Contains("ResolveCanonicalRuntimeModel", source, StringComparison.Ordinal); + Assert.Contains("CanonicalIngressKind.SclFile", canonical, StringComparison.Ordinal); + Assert.Contains("PrepareCanonicalStaticAcquisitionSmartAsync", source, StringComparison.Ordinal); + Assert.Contains("smart.Coverage.Segments", source, StringComparison.Ordinal); + Assert.DoesNotContain("SelectMany(configured => discovery.ReportInventory.ReportControls", source, StringComparison.Ordinal); + Assert.DoesNotContain("Iec61850StaticRcbReferenceMatcher.MatchRank", source, StringComparison.Ordinal); } [Fact] - public void IndexedRcbFamily_PrefersNonOccupiedConcreteInstance() + public void ConcreteRcbSelection_IsEngineEvidenceDriven_AndNeverNameFabricated() { var source = Read("Services/NativeIec61850Client.StaticDataSetReporting.cs"); + var sclClient = Read("Services/NativeIec61850Client.SclAssisted.cs"); - Assert.Contains("matchedLiveCandidates", source, StringComparison.Ordinal); - Assert.Contains("evaluatedLiveCandidates", source, StringComparison.Ordinal); - Assert.Contains("MmsRcbAvailabilityEvaluator.Evaluate", source, StringComparison.Ordinal); Assert.Contains("callerOwnedRcbReferences", source, StringComparison.Ordinal); - Assert.Contains("MmsReportSubscriptionPlanner.IsExplicitlyEnabled(item.Candidate)", source, StringComparison.Ordinal); - Assert.Contains("MmsReportSubscriptionPlanner.IsReservedByOtherClient(item.Candidate)", source, StringComparison.Ordinal); - Assert.Contains("MmsReportSubscriptionPlanner.IsExplicitlyDisabled(item.Candidate)", source, StringComparison.Ordinal); - Assert.Contains("StaticRcbAvailabilityRank", source, StringComparison.Ordinal); - Assert.Contains("Owner={item.Candidate.Owner}", source, StringComparison.Ordinal); - Assert.Contains("every concrete instance was explicitly unavailable/in-use", source, StringComparison.Ordinal); - Assert.Contains("Static mode will not steal an occupied RCB", source, StringComparison.Ordinal); - Assert.Contains("prefer exact identity, caller-owned/known-safe state", source, StringComparison.Ordinal); + Assert.Contains("ExactLiveReportControlReferences", source, StringComparison.Ordinal); + Assert.Contains("MmsConfiguredStaticRcbEligibilityPolicy.Evaluate", source, StringComparison.Ordinal); + Assert.Contains("MmsConfiguredStaticRcbEligibilityKind.CallerOwned", source, StringComparison.Ordinal); + Assert.Contains("MmsConfiguredStaticRcbEligibilityKind.ExplicitFree", source, StringComparison.Ordinal); + Assert.Contains("MmsConfiguredStaticRcbEligibilityKind.ReducedMissingReservationEvidence", source, StringComparison.Ordinal); + Assert.DoesNotContain("StaticRcbAvailabilityRank", source, StringComparison.Ordinal); + Assert.DoesNotContain("ConcreteFirstStaticRcbReference", sclClient, StringComparison.Ordinal); + Assert.DoesNotContain("BuildTrustedSclReportInventory", sclClient, StringComparison.Ordinal); } [Fact] From 384880eab28357b8b635e2f4dff58fa0f45ca314 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 7 Oct 2026 17:09:58 +0700 Subject: [PATCH 08/21] test(reporting): align report-only regression with engine canonical consumer (#446) --- ...ticDataSetReportOnlyModeRegressionTests.cs | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/tests/ARSAS.Tests/StaticDataSetReportOnlyModeRegressionTests.cs b/tests/ARSAS.Tests/StaticDataSetReportOnlyModeRegressionTests.cs index 8537a526d..dc1870cdc 100644 --- a/tests/ARSAS.Tests/StaticDataSetReportOnlyModeRegressionTests.cs +++ b/tests/ARSAS.Tests/StaticDataSetReportOnlyModeRegressionTests.cs @@ -92,19 +92,21 @@ public void StaticRcbMatcher_ExactIdentityAlwaysRanksBeforeIndexedFamily() } [Fact] - public void DeterministicStaticPlanner_PreservesSclConfigurationAndConcreteLiveRcbAuthority() + public void DeterministicStaticPlanner_DelegatesConfigurationAndLiveRcbAuthorityToEngine() { var source = File.ReadAllText(FindRepoFile("Services/NativeIec61850Client.StaticDataSetReporting.cs")); + var canonical = File.ReadAllText(FindRepoFile("Services/NativeIec61850Client.CanonicalAcquisition.cs")); Assert.Contains("device.SclWorkspace?.DesignModel ?? device.LiveDiscoveryModel", source, StringComparison.Ordinal); - Assert.Contains("var configurationModel = projectionModel", source, StringComparison.Ordinal); - Assert.Contains("Iec61850StaticRcbReferenceMatcher.MatchRank", source, StringComparison.Ordinal); - Assert.Contains("SelectMany(configured => discovery.ReportInventory.ReportControls", source, StringComparison.Ordinal); - Assert.Contains("ReportControlReference = concreteReportReference", source, StringComparison.Ordinal); - Assert.Contains("Install InformationReport receiver before enabling the RCB", source, StringComparison.Ordinal); - Assert.Contains("enable RptEna, then request GI after receiver registration", source, StringComparison.Ordinal); - Assert.DoesNotContain("configurationModels", source, StringComparison.Ordinal); - Assert.DoesNotContain("var configured = configuredReports[0]", source, StringComparison.Ordinal); + Assert.Contains("ResolveCanonicalRuntimeModel", source, StringComparison.Ordinal); + Assert.Contains("PrepareCanonicalStaticAcquisitionSmartAsync", source, StringComparison.Ordinal); + Assert.Contains("MmsConfiguredStaticRcbEligibilityPolicy.Evaluate", source, StringComparison.Ordinal); + Assert.Contains("ReportControlReference = selected.Snapshot.Reference", source, StringComparison.Ordinal); + Assert.Contains("StartConfiguredStaticReportMonitorAsync", source, StringComparison.Ordinal); + Assert.Contains("traffic proof remains asynchronous", source, StringComparison.OrdinalIgnoreCase); + Assert.Contains("CanonicalLiveModelAdapter.FromLiveDiscovery", canonical, StringComparison.Ordinal); + Assert.DoesNotContain("Iec61850StaticRcbReferenceMatcher.MatchRank", source, StringComparison.Ordinal); + Assert.DoesNotContain("SelectMany(configured => discovery.ReportInventory.ReportControls", source, StringComparison.Ordinal); Assert.DoesNotContain("AllowDynamicDataSetWrites = true", source, StringComparison.Ordinal); Assert.DoesNotContain("PollingPointKeys = points.Select", source, StringComparison.Ordinal); } From 3d108114f7c9d5aa15fcb02a9e01484074aad1b8 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 7 Oct 2026 17:10:08 +0700 Subject: [PATCH 09/21] test(ci): align mainline guard wording with accepted engine authority --- .../SmartDiscoveryProductionPromotionRegressionTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ARSAS.Tests/SmartDiscoveryProductionPromotionRegressionTests.cs b/tests/ARSAS.Tests/SmartDiscoveryProductionPromotionRegressionTests.cs index c91bb777c..690c1df39 100644 --- a/tests/ARSAS.Tests/SmartDiscoveryProductionPromotionRegressionTests.cs +++ b/tests/ARSAS.Tests/SmartDiscoveryProductionPromotionRegressionTests.cs @@ -81,7 +81,7 @@ public void R10_DedicatedMainlineGateRequiresPhysicalAuthorityAndExactMergedEngi Assert.Contains("engineModelAndScl.mainMerge", workflow, StringComparison.Ordinal); Assert.Contains("physicalTestedCommit", workflow, StringComparison.Ordinal); Assert.Contains("mergedMainTree", workflow, StringComparison.Ordinal); - Assert.Contains("Checkout exact merged ARIEC61850 authority", workflow, StringComparison.Ordinal); + Assert.Contains("Checkout exact accepted ARIEC61850 integration authority", workflow, StringComparison.Ordinal); Assert.Contains("Build and test ARSAS candidate", workflow, StringComparison.Ordinal); Assert.DoesNotContain("perf/smart-ied-discovery", workflow, StringComparison.Ordinal); Assert.DoesNotContain("EvidenceEngineBaselineCommit", workflow, StringComparison.Ordinal); From 290d5ba5b08818d702b413cc05ab32b479bf7afc Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 7 Oct 2026 17:10:30 +0700 Subject: [PATCH 10/21] test(interop): preserve physical baseline while allowing explicit code trial pin (#446) --- .../CanonicalLiveSclExportRegressionTests.cs | 80 +++++++++++-------- 1 file changed, 47 insertions(+), 33 deletions(-) diff --git a/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs b/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs index 4905506d9..049395957 100644 --- a/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs +++ b/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs @@ -401,44 +401,58 @@ public void SourceClean_GuardsEveryTrackedFileWithoutWholeFileExceptions() [Fact] - public void EngineLock_PreservesPhysicalSclRepairBaselineAcrossCandidatePins() + public void EngineLock_PreservesPhysicalSclRepairBaselineAcrossCodeVerifiedInteropTrial() { var lockFile = File.ReadAllText(FindRepoFile("engines/ARIEC61850.lock.json")); - - Assert.Contains( - "\"mergedMainCommit\": \"648124097621046f5f127ceb1cf853fea54db730\"", - lockFile, - StringComparison.Ordinal); - Assert.Contains( - "\"physicalTestedCommit\": \"9935d6902d786cc69b299260fe36b835944d5e81\"", - lockFile, - StringComparison.Ordinal); - Assert.Contains("\"sourcePullRequest\": 143", lockFile, StringComparison.Ordinal); - Assert.Contains("\"previousStablePin\"", lockFile, StringComparison.Ordinal); - Assert.Contains("\"sourcePullRequest\": 135", lockFile, StringComparison.Ordinal); - Assert.Contains("exact association request bytes accepted by the IED", lockFile, StringComparison.Ordinal); - Assert.Contains("accepted COTP destination selector", lockFile, StringComparison.Ordinal); - Assert.Contains("runtime-mutable", lockFile, StringComparison.OrdinalIgnoreCase); - Assert.Contains("DataSet/ConfRev/domain/LN/buffered identity", lockFile, StringComparison.Ordinal); - Assert.Contains("full-model SCL", lockFile, StringComparison.Ordinal); - Assert.Contains("CDC-aware WYE/DEL/SEQ SDO", lockFile, StringComparison.Ordinal); - Assert.Contains("FC ownership", lockFile, StringComparison.Ordinal); - Assert.Contains("TCTR/TVTR/LTIM/EEName/MltLev", lockFile, StringComparison.Ordinal); - Assert.Contains("LTRK service-tracking", lockFile, StringComparison.Ordinal); - Assert.Contains("Edition-1 schema downgrade protection", lockFile, StringComparison.Ordinal); - Assert.Contains("TypeSpecification declaration order", lockFile, StringComparison.Ordinal); - Assert.Contains("SG/SE as setting data", lockFile, StringComparison.Ordinal); - Assert.Contains("MHAI THD phase groups as WYE/CMV", lockFile, StringComparison.Ordinal); - Assert.Contains("bounded FC-read policy", lockFile, StringComparison.Ordinal); - Assert.Contains("complete PR #134 smart-discovery performance head", lockFile, StringComparison.Ordinal); - Assert.Contains("Production promotion remains fail-closed", lockFile, StringComparison.Ordinal); - Assert.Contains( - "\"commit\": \"4467124775d8d9d76f3db194f9fbfd97144767a8\"", - lockFile, - StringComparison.Ordinal); + using var document = System.Text.Json.JsonDocument.Parse(lockFile); + var root = document.RootElement; + + Assert.Equal( + "9c5292570f55dd81be1b3a6b56f937e5ba1ed276", + root.GetProperty("commit").GetString()); + Assert.Equal(148, root.GetProperty("sourcePullRequest").GetInt32()); + + Assert.Equal( + "648124097621046f5f127ceb1cf853fea54db730", + root.GetProperty("mergedMainCommit").GetString()); + Assert.Equal( + "9935d6902d786cc69b299260fe36b835944d5e81", + root.GetProperty("physicalTestedCommit").GetString()); + + var stable = root.GetProperty("previousStablePin"); + Assert.Equal( + "648124097621046f5f127ceb1cf853fea54db730", + stable.GetProperty("commit").GetString()); + Assert.Equal(135, stable.GetProperty("sourcePullRequest").GetInt32()); + + var association = root.GetProperty("sclAssociationInteroperability"); + Assert.Equal(143, association.GetProperty("sourcePullRequest").GetInt32()); + Assert.Equal( + "84e9820e5a32690475960e49d5ef74e6637847fd", + association.GetProperty("testedEngineCommit").GetString()); + Assert.Equal( + "e5deed1d8aa11d97991695c6e390baafea7ab797", + association.GetProperty("mergedEngineCommit").GetString()); + Assert.True(association.GetProperty("preservesR10DiscoveryModelBaseline").GetBoolean()); + Assert.True(association.GetProperty("fullDiscoveryFallbackForbidden").GetBoolean()); + + var trial = root.GetProperty("smartInteroperabilityCodeTrial"); + Assert.Equal("code-verified-not-physical", trial.GetProperty("status").GetString()); + Assert.Equal( + root.GetProperty("commit").GetString(), + trial.GetProperty("exactCommit").GetString()); + Assert.Equal(759, trial.GetProperty("ciRun").GetInt32()); + Assert.True(trial.GetProperty("physicalPromotionRequired").GetBoolean()); + Assert.True(trial.GetProperty("vendorBranchingForbidden").GetBoolean()); + + var previousTrial = root.GetProperty("previousTrialPin"); + Assert.Equal( + "4467124775d8d9d76f3db194f9fbfd97144767a8", + previousTrial.GetProperty("commit").GetString()); } + private static string FindRepoFile(string relativePath) { DirectoryInfo? directory = new(AppContext.BaseDirectory); From 06aecfd0d6edc34c0e9b317825239718de43a719 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 7 Oct 2026 17:11:48 +0700 Subject: [PATCH 11/21] refactor(runtime): converge Open SCL and Discovery static activation path (#446) --- Services/Iec61850MonitorRuntime.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Services/Iec61850MonitorRuntime.cs b/Services/Iec61850MonitorRuntime.cs index 5b56d82a5..d7f58f37b 100644 --- a/Services/Iec61850MonitorRuntime.cs +++ b/Services/Iec61850MonitorRuntime.cs @@ -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) { From 382910f92a2a187ba86c079320de675083227a54 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 7 Oct 2026 17:12:07 +0700 Subject: [PATCH 12/21] test(runtime): lock one activation path for Open SCL and Discovery (#446) --- .../SclGoldenWireIntegrationContractTests.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/ARSAS.Tests/SclGoldenWireIntegrationContractTests.cs b/tests/ARSAS.Tests/SclGoldenWireIntegrationContractTests.cs index ebe66d116..f5e1576da 100644 --- a/tests/ARSAS.Tests/SclGoldenWireIntegrationContractTests.cs +++ b/tests/ARSAS.Tests/SclGoldenWireIntegrationContractTests.cs @@ -25,12 +25,15 @@ public void Runtime_FastPlay_PrefersVerifiedSclWithoutDiscoveryFallback() } [Fact] - public void Runtime_TrustedSclStaticReporting_BypassesHybridAndLegacyStartPaths() + public void Runtime_OpenSclAndDiscovery_UseOneEngineAuthoritativeActivationPath() { var source = File.ReadAllText(FindRepoFile("Services/Iec61850MonitorRuntime.cs")); - Assert.Contains("session.StaticDataSetReportOnly && session.Client.HasTrustedSclOnlineAuthority", source, StringComparison.Ordinal); - Assert.Contains("StartTrustedSclStaticReportMonitorAsync", source, StringComparison.Ordinal); - Assert.Contains("DataSet membership and RCB identity remain SCL-authoritative", source, StringComparison.Ordinal); + + Assert.Contains("plan.IsEngineAuthoritative", source, StringComparison.Ordinal); + Assert.Contains("StartHybridReportMonitorAsync", source, StringComparison.Ordinal); + Assert.Contains("Discovery IP and Open SCL converge before execution", source, StringComparison.Ordinal); + Assert.DoesNotContain("session.StaticDataSetReportOnly && session.Client.HasTrustedSclOnlineAuthority", source, StringComparison.Ordinal); + Assert.DoesNotContain("StartTrustedSclStaticReportMonitorAsync(plan", source, StringComparison.Ordinal); } [Fact] From b4ee0589637a3d4384248be0d74a6d36e8aff37c Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 7 Oct 2026 17:15:06 +0700 Subject: [PATCH 13/21] refactor(runtime): converge Open SCL and Discovery static planning path (#446) --- Services/Iec61850MonitorRuntime.cs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/Services/Iec61850MonitorRuntime.cs b/Services/Iec61850MonitorRuntime.cs index d7f58f37b..29995cc2f 100644 --- a/Services/Iec61850MonitorRuntime.cs +++ b/Services/Iec61850MonitorRuntime.cs @@ -1017,20 +1017,6 @@ private async Task> BuildReportPlansForCurrentA IReadOnlyList 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; From c04dd11e236fd2463f436beca56814d814e85f84 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 7 Oct 2026 17:15:16 +0700 Subject: [PATCH 14/21] refactor(reporting): remove obsolete ARSAS RCB availability ranking (#446) --- Services/NativeIec61850Client.StaticDataSetReporting.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Services/NativeIec61850Client.StaticDataSetReporting.cs b/Services/NativeIec61850Client.StaticDataSetReporting.cs index cd540db7e..d890297ab 100644 --- a/Services/NativeIec61850Client.StaticDataSetReporting.cs +++ b/Services/NativeIec61850Client.StaticDataSetReporting.cs @@ -487,15 +487,6 @@ private static bool TryVerifyStaticDataSetMemberOrder( return true; } - private static int StaticRcbAvailabilityRank(ArMms.MmsRcbOperationalAvailability availability) - => availability switch - { - ArMms.MmsRcbOperationalAvailability.UsedByCaller => 0, - ArMms.MmsRcbOperationalAvailability.Available => 1, - ArMms.MmsRcbOperationalAvailability.Unknown => 2, - _ => int.MaxValue - }; - private static bool SameStaticReference(string? left, string? right) => string.Equals( NormalizeStaticReference(left), From e859ec26914efe87b5d118ed1ececa941260cf9c Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 7 Oct 2026 17:16:28 +0700 Subject: [PATCH 15/21] refactor(scl): remove retired trusted-SCL RCB runtime cache (#446) --- Services/NativeIec61850Client.SclAssisted.cs | 27 -------------------- 1 file changed, 27 deletions(-) diff --git a/Services/NativeIec61850Client.SclAssisted.cs b/Services/NativeIec61850Client.SclAssisted.cs index 051daeffb..a2af3c09c 100644 --- a/Services/NativeIec61850Client.SclAssisted.cs +++ b/Services/NativeIec61850Client.SclAssisted.cs @@ -28,8 +28,6 @@ internal sealed class TrustedSclInitialValue public sealed partial class NativeIec61850Client { - private readonly Dictionary _trustedSclDataSetDirectories = - new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary _trustedSclInitialValues = // IEC 61850 object/member identity is case-sensitive. Edition 2 tracking // can legally expose distinct paths such as "...t" and "...T". @@ -39,24 +37,6 @@ public sealed partial class NativeIec61850Client internal bool HasTrustedSclOnlineAuthority => _trustedSclOnlineAuthorityActive; internal int TrustedSclInitialValueCount => _trustedSclOnlineAuthorityActive ? _trustedSclInitialValues.Count : 0; - internal IReadOnlyList TrustedSclReportControls - => _trustedSclOnlineAuthorityActive && _lastDiscovery is not null - ? _lastDiscovery.ReportInventory.ReportControls - : Array.Empty(); - - internal bool TryGetTrustedSclDataSetDirectory( - string dataSetReference, - out ArMms.MmsDataSetDirectoryResult directory) - { - directory = null!; - if (!_trustedSclOnlineAuthorityActive) - return false; - - return _trustedSclDataSetDirectories.TryGetValue( - NormalizeTrustedSclReference(dataSetReference), - out directory!); - } - internal bool TryGetTrustedSclInitialValue( string reference, out TrustedSclInitialValue value) @@ -73,7 +53,6 @@ internal bool TryGetTrustedSclInitialValue( private void ResetTrustedSclOnlineAuthority() { _trustedSclOnlineAuthorityActive = false; - _trustedSclDataSetDirectories.Clear(); _trustedSclInitialValues.Clear(); } @@ -284,12 +263,6 @@ or ArMms.InitialFcReadExecutionStatus.TimedOut _canonicalRuntimeModel ?? throw new InvalidOperationException("Canonical SCL runtime model was lost after successful import.")); var dataSetDirectories = BuildModelDataSetDirectories(_liveModel, "TrustedScl"); - foreach (var directory in dataSetDirectories) - { - _trustedSclDataSetDirectories[ - NormalizeTrustedSclReference(directory.DataSetReference)] = directory; - } - var projectedInitialValueKeys = new HashSet(StringComparer.Ordinal); foreach (var leaf in initialRead.Batches .SelectMany(batch => batch.Projections) From 70061f532e468248c440328d1668b044cdc533eb Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 7 Oct 2026 17:16:34 +0700 Subject: [PATCH 16/21] refactor(reporting): remove parallel trusted-SCL reporting runtime (#446) --- ...ec61850Client.TrustedSclStaticReporting.cs | 217 ------------------ 1 file changed, 217 deletions(-) delete mode 100644 Services/NativeIec61850Client.TrustedSclStaticReporting.cs diff --git a/Services/NativeIec61850Client.TrustedSclStaticReporting.cs b/Services/NativeIec61850Client.TrustedSclStaticReporting.cs deleted file mode 100644 index 63224d411..000000000 --- a/Services/NativeIec61850Client.TrustedSclStaticReporting.cs +++ /dev/null @@ -1,217 +0,0 @@ -using ArIED61850Tester.Models; -using ArMms = AR.Iec61850.Mms; - -namespace ArIED61850Tester.Services; - -/// -/// Golden-wire static-report adapter used only after a successful trusted-SCL online -/// connection. DataSet membership and configured RCB identity come from the exact SCL -/// source that established the association. No live DataSet-directory browse or dynamic -/// DataSet mutation is permitted on this path. Normal Play requests one explicit one-shot -/// GI after the receiver is installed and the trusted static RCB is armed so the initial -/// DataSet image is delivered without cyclic MMS polling. -/// -public sealed partial class NativeIec61850Client -{ - public async Task StartTrustedSclStaticReportMonitorAsync( - ReportControlPlan plan, - CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(plan); - cancellationToken.ThrowIfCancellationRequested(); - - if (!HasTrustedSclOnlineAuthority) - { - return TrustedSclStaticFailure( - plan, - "Trusted SCL online authority is not active for this MMS association.", - "TrustedSclAuthorityUnavailable"); - } - - if (!_session.IsMmsInitiated) - { - return TrustedSclStaticFailure( - plan, - $"Trusted SCL static report monitor requires an initiated MMS association. Current state: {_session.State}.", - "TransportUnavailable"); - } - - if (_reportMonitorSessions.ContainsKey(plan.PlanId)) - { - return new NativeReportMonitorStartResult - { - IsSuccess = true, - PlanId = plan.PlanId, - Message = $"Trusted SCL static report monitor already active for {plan.DisplayReference}.", - ReportControlReference = plan.ReportControlReference, - DataSetReference = plan.DataSetReference, - AcquisitionLabel = $"Trusted SCL: {(plan.Buffered ? "StaticBrcb" : "StaticUrcb")}", - CoveredReferences = _reportMonitorCoverage.TryGetValue(plan.PlanId, out var existingCoverage) - ? existingCoverage - : Array.Empty() - }; - } - - if (string.IsNullOrWhiteSpace(plan.DataSetReference) || - !TryGetTrustedSclDataSetDirectory(plan.DataSetReference, out var directory) || - !directory.IsSuccess || - directory.Members.Count == 0) - { - return TrustedSclStaticFailure( - plan, - $"Trusted SCL DataSet '{plan.DataSetReference}' is missing or has no ordered members. " + - "The client refused network DataSet-directory discovery and did not arm an RCB.", - "TrustedSclDataSetUnavailable"); - } - - var configuredReference = (plan.ReportControlReference ?? string.Empty).Trim(); - var candidates = TrustedSclReportControls - .Where(candidate => - string.IsNullOrWhiteSpace(candidate.DataSetReference) || - SameStaticReference(candidate.DataSetReference, directory.DataSetReference)) - .Select(candidate => new - { - Candidate = candidate, - Rank = string.IsNullOrWhiteSpace(configuredReference) - ? 0 - : Iec61850StaticRcbReferenceMatcher.MatchRank(configuredReference, candidate.Reference) - }) - .Where(item => item.Rank != int.MaxValue) - .OrderBy(item => item.Rank) - .ThenByDescending(item => item.Candidate.Buffered) - .ThenBy(item => item.Candidate.Reference, StringComparer.OrdinalIgnoreCase) - .ToArray(); - - if (candidates.Length == 0) - { - return TrustedSclStaticFailure( - plan, - $"No trusted SCL RCB matches configured reference '{plan.ReportControlReference}' and DataSet '{directory.DataSetReference}'. " + - "No peer RCB substitution or live RCB directory discovery was attempted.", - "TrustedSclRcbUnavailable"); - } - - var bestRank = candidates[0].Rank; - var best = candidates.Where(item => item.Rank == bestRank).ToArray(); - if (best.Length != 1) - { - return TrustedSclStaticFailure( - plan, - $"Trusted SCL RCB selection is ambiguous for '{plan.ReportControlReference}': {best.Length} equally ranked candidate(s). " + - "The client failed closed without enabling any RCB.", - "TrustedSclRcbAmbiguous"); - } - - var rcb = CloneReportControlForPlanning(best[0].Candidate); - if (string.IsNullOrWhiteSpace(rcb.DataSetReference)) - rcb.DataSetReference = directory.DataSetReference; - if (!SameStaticReference(rcb.DataSetReference, directory.DataSetReference)) - { - return TrustedSclStaticFailure( - plan, - $"Trusted SCL RCB {rcb.Reference} binds DataSet '{rcb.DataSetReference}', not '{directory.DataSetReference}'. " + - "RCB activation was withheld.", - "TrustedSclRcbDataSetMismatch"); - } - - var subscription = new ArMms.MmsReportSubscriptionPlan - { - Mode = ArMms.MmsReportSubscriptionPlanMode.StaticDataSet, - Status = ArMms.MmsReportSubscriptionPlanStatus.ReadyRequiresWrite, - ReportControl = rcb, - DataSetReference = directory.DataSetReference, - Members = directory.Members, - DynamicPoints = Array.Empty(), - Steps = new[] - { - $"Use trusted SCL RCB {rcb.Reference} and DataSet {directory.DataSetReference}.", - $"Use {directory.Members.Count} ordered DataSet member(s) from the verified SCL source.", - "Install InformationReport receiver before RCB activation.", - "Primary wire sequence: whole-RCB Read, optional URCB Resv, RptEna=true, two whole-RCB readbacks.", - "BRCB ResvTms is retry-only after a real direct-RptEna rejection; request one explicit GI=true after activation for the initial image." - }, - Warnings = Array.Empty() - }; - - var coveredReferences = ExtractSubscriptionMemberReferences(subscription.Members); - var start = await RunMmsOperationAsync( - () => _session.StartStaticSclReportMonitorAsync( - subscription, - triggerGeneralInterrogation: true, - cancellationToken), - cancellationToken).ConfigureAwait(false); - - var warnings = start.Warnings - .Concat(subscription.Warnings) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToArray(); - - if (!start.IsSuccess || start.Session is null) - { - return new NativeReportMonitorStartResult - { - IsSuccess = false, - PlanId = plan.PlanId, - Message = $"Trusted SCL static report activation failed for {plan.DisplayReference}: {start.Message}", - SubscriptionSummary = subscription.Summary, - MemberCount = subscription.Members.Count, - WriteStepCount = start.WriteSteps.Count, - UsedDynamicDataSet = false, - DynamicAttempted = false, - DynamicAttemptState = "NotApplicable", - FailureReason = "TrustedSclStaticActivationFailed", - ReportControlReference = plan.ReportControlReference, - DataSetReference = plan.DataSetReference, - CoveredReferences = coveredReferences, - Warnings = warnings - }; - } - - if (!string.IsNullOrWhiteSpace(start.Session.ReportControl.Reference)) - plan.ReportControlReference = start.Session.ReportControl.Reference; - if (!string.IsNullOrWhiteSpace(start.Session.Plan.DataSetReference)) - plan.DataSetReference = start.Session.Plan.DataSetReference; - plan.Buffered = start.Session.ReportControl.Buffered; - plan.IsEngineAuthoritative = true; - plan.EngineAcquisitionKind = plan.Buffered ? "StaticBrcb" : "StaticUrcb"; - - _reportMonitorSessions[plan.PlanId] = start.Session; - _reportMonitorCoverage[plan.PlanId] = coveredReferences; - - return new NativeReportMonitorStartResult - { - IsSuccess = true, - PlanId = plan.PlanId, - Message = $"Trusted SCL {plan.EngineAcquisitionKind} monitor active with one-shot GI startup request. {start.Message}", - SubscriptionSummary = subscription.Summary, - MemberCount = subscription.Members.Count, - WriteStepCount = start.WriteSteps.Count, - UsedDynamicDataSet = false, - DynamicAttempted = false, - DynamicAttemptState = "NotApplicable", - ReportControlReference = plan.ReportControlReference, - DataSetReference = plan.DataSetReference, - AcquisitionLabel = $"Trusted SCL: {plan.EngineAcquisitionKind}", - CoveredReferences = coveredReferences, - Warnings = warnings - }; - } - - private static NativeReportMonitorStartResult TrustedSclStaticFailure( - ReportControlPlan plan, - string message, - string failureReason) - => new() - { - IsSuccess = false, - PlanId = plan.PlanId, - Message = message, - UsedDynamicDataSet = false, - DynamicAttempted = false, - DynamicAttemptState = "NotApplicable", - FailureReason = failureReason, - ReportControlReference = plan.ReportControlReference, - DataSetReference = plan.DataSetReference, - CoveredReferences = Array.Empty() - }; -} From 3914a3a44e4553d39f9b95c3423943b39b5099ce Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 7 Oct 2026 17:16:48 +0700 Subject: [PATCH 17/21] test(reporting): retire duplicate trusted-SCL activation contract (#446) --- ...DeterministicStaticReportPathRegressionTests.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/ARSAS.Tests/DeterministicStaticReportPathRegressionTests.cs b/tests/ARSAS.Tests/DeterministicStaticReportPathRegressionTests.cs index 0bb3f0262..6d9d2b0ca 100644 --- a/tests/ARSAS.Tests/DeterministicStaticReportPathRegressionTests.cs +++ b/tests/ARSAS.Tests/DeterministicStaticReportPathRegressionTests.cs @@ -28,21 +28,21 @@ public void StaticPath_UsesEngineCanonicalHotPath_AndNeverEnablesPolling() } [Fact] - public void TrustedSclStaticPath_RequestsOneShotGi_WhileSafeTrialRemainsReadOnly() + public void UnifiedConfiguredStaticPath_RequestsOneShotGi_WhileSafeTrialRemainsReadOnly() { - var trustedStatic = Read("Services/NativeIec61850Client.TrustedSclStaticReporting.cs"); + var staticReporting = Read("Services/NativeIec61850Client.StaticDataSetReporting.cs"); + var runtime = Read("Services/Iec61850MonitorRuntime.cs"); var safeTrial = Read("Services/SclSafeTrialRunner.cs"); - Assert.Contains("triggerGeneralInterrogation: true", trustedStatic, StringComparison.Ordinal); - Assert.Contains("one explicit GI=true", trustedStatic, StringComparison.Ordinal); - Assert.Contains("one-shot GI startup request", trustedStatic, StringComparison.Ordinal); - Assert.DoesNotContain("triggerGeneralInterrogation: false", trustedStatic, StringComparison.Ordinal); + Assert.Contains("StartConfiguredStaticReportMonitorAsync", staticReporting, StringComparison.Ordinal); + Assert.Contains("triggerGeneralInterrogation: true", staticReporting, StringComparison.Ordinal); + Assert.DoesNotContain("StartTrustedSclStaticReportMonitorAsync", runtime, StringComparison.Ordinal); Assert.Contains("readOnly = true", safeTrial, StringComparison.Ordinal); Assert.Contains("writesAllowed = false", safeTrial, StringComparison.Ordinal); Assert.Contains("reportEnableAllowed = false", safeTrial, StringComparison.Ordinal); Assert.Contains("dynamicDataSetAllowed = false", safeTrial, StringComparison.Ordinal); - Assert.DoesNotContain("StartTrustedSclStaticReportMonitorAsync", safeTrial, StringComparison.Ordinal); + Assert.DoesNotContain("StartConfiguredStaticReportMonitorAsync", safeTrial, StringComparison.Ordinal); } [Fact] From 43290c329395a5cb8dbc981ef5fd841e703ddae4 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 7 Oct 2026 17:17:00 +0700 Subject: [PATCH 18/21] test(scl): bind golden-wire contract to unified configured-static runtime (#446) --- .../SclGoldenWireIntegrationContractTests.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/ARSAS.Tests/SclGoldenWireIntegrationContractTests.cs b/tests/ARSAS.Tests/SclGoldenWireIntegrationContractTests.cs index f5e1576da..d6bb78dfe 100644 --- a/tests/ARSAS.Tests/SclGoldenWireIntegrationContractTests.cs +++ b/tests/ARSAS.Tests/SclGoldenWireIntegrationContractTests.cs @@ -37,17 +37,17 @@ public void Runtime_OpenSclAndDiscovery_UseOneEngineAuthoritativeActivationPath( } [Fact] - public void TrustedSclStaticAdapter_HasGoldenWireSafetyContract() + public void UnifiedConfiguredStaticAdapter_HasGoldenWireSafetyContract() { - var source = File.ReadAllText(FindRepoFile("Services/NativeIec61850Client.TrustedSclStaticReporting.cs")); - Assert.Contains("TryGetTrustedSclDataSetDirectory", source, StringComparison.Ordinal); - Assert.Contains("StartStaticSclReportMonitorAsync", source, StringComparison.Ordinal); + var source = File.ReadAllText(FindRepoFile("Services/NativeIec61850Client.StaticDataSetReporting.cs")); + + Assert.Contains("PrepareCanonicalStaticAcquisitionSmartAsync", source, StringComparison.Ordinal); + Assert.Contains("StartConfiguredStaticReportMonitorAsync", source, StringComparison.Ordinal); Assert.Contains("triggerGeneralInterrogation: true", source, StringComparison.Ordinal); - Assert.Contains("one explicit GI=true", source, StringComparison.Ordinal); - Assert.DoesNotContain("triggerGeneralInterrogation: false", source, StringComparison.Ordinal); - Assert.DoesNotContain("GetDataSetDirectoriesAsync", source, StringComparison.Ordinal); + Assert.Contains("Install InformationReport receiver before RptEna mutation", source, StringComparison.Ordinal); + Assert.DoesNotContain("EnsureDiscoveryForReportingAsync(cancellationToken)", source, StringComparison.Ordinal); Assert.DoesNotContain("DefineNamedVariableList", source, StringComparison.Ordinal); - Assert.DoesNotContain("StartPersistentReportMonitorAsync", source, StringComparison.Ordinal); + Assert.DoesNotContain("StartPersistentReportMonitorClientCompatibleAsync", source, StringComparison.Ordinal); } [Fact] From 825902f71363e25fe8f380810d691cb7bc0846f8 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 7 Oct 2026 18:20:22 +0700 Subject: [PATCH 19/21] ci(interoperability): separate code-trial engine from physical ancestry (#446) --- .../interoperability-reference-guard.yml | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/.github/workflows/interoperability-reference-guard.yml b/.github/workflows/interoperability-reference-guard.yml index 9b1df9e8f..788ce0318 100644 --- a/.github/workflows/interoperability-reference-guard.yml +++ b/.github/workflows/interoperability-reference-guard.yml @@ -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 @@ -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 From 02035266499bffe3a37ae299752bbc0b149bf261 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 7 Oct 2026 18:20:43 +0700 Subject: [PATCH 20/21] ci(readiness): allow stacked CodeVerified trial without weakening mainline physical gate (#446) --- .../smart-discovery-mainline-readiness.yml | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/.github/workflows/smart-discovery-mainline-readiness.yml b/.github/workflows/smart-discovery-mainline-readiness.yml index 8291c1fb7..8de61d72b 100644 --- a/.github/workflows/smart-discovery-mainline-readiness.yml +++ b/.github/workflows/smart-discovery-mainline-readiness.yml @@ -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 @@ -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 @@ -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 From 271ad92a9865831518f61cb8bb8632a81f5cbc17 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 7 Oct 2026 18:24:15 +0700 Subject: [PATCH 21/21] test(ci): lock stacked trial vs mainline physical authority semantics (#446) --- ...teroperabilityTrialGuardRegressionTests.cs | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 tests/ARSAS.Tests/SmartInteroperabilityTrialGuardRegressionTests.cs diff --git a/tests/ARSAS.Tests/SmartInteroperabilityTrialGuardRegressionTests.cs b/tests/ARSAS.Tests/SmartInteroperabilityTrialGuardRegressionTests.cs new file mode 100644 index 000000000..7bd4e6739 --- /dev/null +++ b/tests/ARSAS.Tests/SmartInteroperabilityTrialGuardRegressionTests.cs @@ -0,0 +1,80 @@ +namespace ARSAS.Tests; + +public sealed class SmartInteroperabilityTrialGuardRegressionTests +{ + [Fact] + public void InteroperabilityGuard_SeparatesCodeTrialFromPhysicalAssociationAuthority() + { + var workflow = Read(".github/workflows/interoperability-reference-guard.yml"); + + Assert.Contains("$lock.smartInteroperabilityCodeTrial", workflow, StringComparison.Ordinal); + Assert.Contains("code-verified-not-physical", workflow, StringComparison.Ordinal); + Assert.Contains("$trial.exactCommit -ne $lock.commit", workflow, StringComparison.Ordinal); + Assert.Contains( + "$lock.sclAssociationInteroperability.testedEngineCommit -ne $association.engine.testedHead", + workflow, + StringComparison.Ordinal); + Assert.Contains( + "Code trial replaced or mutated the physically accepted SCL-association ancestry", + workflow, + StringComparison.Ordinal); + } + + [Fact] + public void MainlineReadiness_AllowsStackedTrialButFailsClosedWhenRetargetedToMain() + { + var workflow = Read(".github/workflows/smart-discovery-mainline-readiness.yml"); + + Assert.Contains("PR_BASE_REF: ${{ github.base_ref }}", workflow, StringComparison.Ordinal); + Assert.Contains("$lock.smartInteroperabilityCodeTrial", workflow, StringComparison.Ordinal); + Assert.Contains("$env:PR_BASE_REF -eq 'main'", workflow, StringComparison.Ordinal); + Assert.Contains( + "CodeVerified smart-interoperability trial cannot pass mainline readiness before fresh physical qualification", + workflow, + StringComparison.Ordinal); + Assert.Contains( + "Stacked consumer trial accepted for CI", + workflow, + StringComparison.Ordinal); + } + + [Fact] + public void TrialLock_PreservesHistoricalPhysicalAuthorityAndExplicitlyRequiresPhysicalPromotion() + { + var lockJson = Read("engines/ARIEC61850.lock.json"); + + using var document = System.Text.Json.JsonDocument.Parse(lockJson); + var root = document.RootElement; + var trial = root.GetProperty("smartInteroperabilityCodeTrial"); + var association = root.GetProperty("sclAssociationInteroperability"); + + Assert.Equal("code-verified-not-physical", trial.GetProperty("status").GetString()); + Assert.True(trial.GetProperty("physicalPromotionRequired").GetBoolean()); + Assert.Equal(root.GetProperty("commit").GetString(), trial.GetProperty("exactCommit").GetString()); + + Assert.Equal(143, association.GetProperty("sourcePullRequest").GetInt32()); + Assert.Equal( + "84e9820e5a32690475960e49d5ef74e6637847fd", + association.GetProperty("testedEngineCommit").GetString()); + Assert.Equal( + "e5deed1d8aa11d97991695c6e390baafea7ab797", + association.GetProperty("mergedEngineCommit").GetString()); + } + + private static string Read(string relativePath) + => File.ReadAllText(FindRepoFile(relativePath)).Replace("\r\n", "\n", StringComparison.Ordinal); + + private static string FindRepoFile(string relativePath) + { + DirectoryInfo? directory = new(AppContext.BaseDirectory); + while (directory is not null) + { + var candidate = Path.Combine(directory.FullName, relativePath); + if (File.Exists(candidate)) + return candidate; + directory = directory.Parent; + } + + throw new FileNotFoundException(relativePath); + } +}