From c3e66a295919eaa5c24a0a95f6fc81fb79530f0d Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 19 Sep 2026 20:22:21 +0700 Subject: [PATCH 01/18] refactor(reporting): make model DataSet directory projection source-neutral --- Services/NativeIec61850Client.SclAssisted.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Services/NativeIec61850Client.SclAssisted.cs b/Services/NativeIec61850Client.SclAssisted.cs index 3c5c17940..ccf683311 100644 --- a/Services/NativeIec61850Client.SclAssisted.cs +++ b/Services/NativeIec61850Client.SclAssisted.cs @@ -256,7 +256,7 @@ or ArMms.InitialFcReadExecutionStatus.TimedOut _liveModel = preparation.InitialReadDesign.Model; var reportInventory = BuildTrustedSclReportInventory(_liveModel); - var dataSetDirectories = BuildTrustedSclDataSetDirectories(_liveModel); + var dataSetDirectories = BuildModelDataSetDirectories(_liveModel, "TrustedScl"); foreach (var directory in dataSetDirectories) { _trustedSclDataSetDirectories[ @@ -455,8 +455,9 @@ private static ArMms.MmsReportInventory BuildTrustedSclReportInventory( return inventory; } - private static IReadOnlyList BuildTrustedSclDataSetDirectories( - LiveIedModelDiscoveryDocument model) + private static IReadOnlyList BuildModelDataSetDirectories( + LiveIedModelDiscoveryDocument model, + string source) => model.DataSets .Select(dataSet => { @@ -467,7 +468,7 @@ private static ArMms.MmsReportInventory BuildTrustedSclReportInventory( dataSet.Name); var members = dataSet.Members .OrderBy(member => member.Index) - .Select(member => BuildTrustedSclDataSetMember(member, domain)) + .Select(member => BuildModelDataSetMember(member, domain, source)) .ToArray(); return new ArMms.MmsDataSetDirectoryResult { @@ -478,14 +479,15 @@ private static ArMms.MmsReportInventory BuildTrustedSclReportInventory( IsDeletable = dataSet.IsDeletable, Members = members, Message = - $"Trusted SCL DataSet authority: {dataSet.Reference} has {members.Length} ordered member(s); no network directory request was sent." + $"{source} DataSet authority: {dataSet.Reference} has {members.Length} ordered member(s); model order is preserved." }; }) .ToArray(); - private static ArMms.MmsDataSetDirectoryMember BuildTrustedSclDataSetMember( + private static ArMms.MmsDataSetDirectoryMember BuildModelDataSetMember( LiveIedDataSetMemberModel member, - string fallbackDomain) + string fallbackDomain, + string source) { var mmsReference = member.MmsReference?.Trim() ?? string.Empty; var slash = mmsReference.IndexOf('/'); @@ -507,7 +509,7 @@ private static ArMms.MmsDataSetDirectoryMember BuildTrustedSclDataSetMember( FunctionalConstraint = member.FunctionalConstraint, LogicalNode = logicalNode, DataObjectPath = dataObjectPath, - Source = "TrustedScl", + Source = source, Confidence = 100 }; } From e2ad4bad2b36195d2f81c82731c9c9c2e913c151 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 19 Sep 2026 20:23:11 +0700 Subject: [PATCH 02/18] fix(reporting): keep canonical DataSet order through live static monitoring --- ...veIec61850Client.StaticDataSetReporting.cs | 96 ++++++++++++++----- 1 file changed, 74 insertions(+), 22 deletions(-) diff --git a/Services/NativeIec61850Client.StaticDataSetReporting.cs b/Services/NativeIec61850Client.StaticDataSetReporting.cs index fbc918f4c..5b0495d10 100644 --- a/Services/NativeIec61850Client.StaticDataSetReporting.cs +++ b/Services/NativeIec61850Client.StaticDataSetReporting.cs @@ -60,6 +60,14 @@ public async Task BuildStaticDataSetReportPlan ? "opened SCL design model" : "live discovery model (online-only)"; + // The model that created the operator-visible DataSet inventory is also the semantic + // member-order authority for InformationReport mapping. Live directory evidence is + // verification only; replacing these members with a second raw network projection can + // re-introduce positional/type ambiguity after discovery has already canonicalized it. + 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 @@ -235,20 +243,31 @@ public async Task BuildStaticDataSetReportPlan if (string.IsNullOrWhiteSpace(liveRcb.DataSetReference)) liveRcb.DataSetReference = dataSetReference; - var directories = await RunMmsOperationAsync( - () => _session.GetDataSetDirectoriesAsync( - new[] { dataSetReference }, - discovery.IedDirectory, - cancellationToken), - cancellationToken).ConfigureAwait(false); - var directory = directories.SingleOrDefault(result => + var modelDirectory = modelDataSetDirectories.SingleOrDefault(result => result.IsSuccess && SameStaticReference(result.DataSetReference, 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."); + continue; + } + + // Smart Discovery already captured GetNamedVariableListAttributes in this same + // association. Reuse that wire evidence instead of issuing a second directory read, + // then require exact count/order identity against the canonical model. + var liveDirectory = discovery.DataSetDirectories.SingleOrDefault(result => + result.IsSuccess && SameStaticReference(result.DataSetReference, dataSetReference)); + if (liveDirectory is null || liveDirectory.Members.Count == 0) + { + warnings.Add( + $"{dataSetReference}: discovery snapshot has no ordered live DataSet directory. RCB was not armed; re-scan is required and MMS process polling remains disabled."); + continue; + } - if (directory is null || directory.Members.Count == 0) + if (!TryVerifyStaticDataSetMemberOrder(modelDirectory, liveDirectory, out var directoryMismatch)) { - var detail = directories.FirstOrDefault()?.Message ?? "no directory response"; warnings.Add( - $"{dataSetReference}: live DataSet directory could not prove an ordered non-empty member list ({detail}). RCB was not armed because report-index mapping would be unsafe; MMS process polling remains disabled."); + $"{dataSetReference}: canonical/live DataSet member order mismatch ({directoryMismatch}). RCB was not armed; unsafe positional report projection and MMS process fallback were both refused."); continue; } @@ -307,12 +326,12 @@ public async Task BuildStaticDataSetReportPlan Status = ArMms.MmsReportSubscriptionPlanStatus.ReadyRequiresWrite, ReportControl = liveRcb, DataSetReference = dataSetReference, - Members = directory.Members, + Members = modelDirectory.Members, DynamicPoints = Array.Empty(), Steps = new[] { $"Verify authoritative configured RCB {configured.Reference} as live object {concreteReportReference}.", - $"Use exact ordered live DataSet directory {dataSetReference} ({directory.Members.Count} members).", + $"Use canonical ordered DataSet members {dataSetReference} ({modelDirectory.Members.Count} members), verified against the discovery snapshot.", "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." @@ -423,22 +442,18 @@ public async Task StartStaticDataSetReportMonito } var coveredReferences = ExtractSubscriptionMemberReferences(subscription.Members); - var attempt = await RunMmsOperationAsync( - () => _session.StartPersistentReportMonitorClientCompatibleAsync( + var start = await RunMmsOperationAsync( + () => _session.StartConfiguredStaticReportMonitorAsync( subscription, triggerGeneralInterrogation: true, - deleteDynamicDataSetOnStop: false, - discovery.IedDirectory, cancellationToken), cancellationToken).ConfigureAwait(false); - var start = attempt.StartResult; 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 { @@ -451,9 +466,9 @@ public async Task StartStaticDataSetReportMonito UsedDynamicDataSet = false, DynamicAttempted = false, DynamicAttemptState = "NotApplicable", - FailureReason = attempt.FailureReason.ToString(), - CleanupAttempted = attempt.CleanupAttempted, - CleanupSucceeded = attempt.CleanupSucceeded, + FailureReason = "ConfiguredStaticActivationFailed", + CleanupAttempted = false, + CleanupSucceeded = true, ReportControlReference = plan.ReportControlReference, DataSetReference = plan.DataSetReference, CoveredReferences = coveredReferences, @@ -508,6 +523,43 @@ private static NativeHybridReportPlanningResult StaticPlanningUnavailable( UncoveredSignalCount = points.Count }; + private static bool TryVerifyStaticDataSetMemberOrder( + ArMms.MmsDataSetDirectoryResult modelDirectory, + ArMms.MmsDataSetDirectoryResult liveDirectory, + out string mismatch) + { + mismatch = string.Empty; + if (modelDirectory.Members.Count != liveDirectory.Members.Count) + { + mismatch = $"count model={modelDirectory.Members.Count}, live={liveDirectory.Members.Count}"; + return false; + } + + for (var index = 0; index < modelDirectory.Members.Count; index++) + { + var modelMember = modelDirectory.Members[index]; + var liveMember = liveDirectory.Members[index]; + var mmsMatches = + !string.IsNullOrWhiteSpace(modelMember.MmsReference) && + !string.IsNullOrWhiteSpace(liveMember.MmsReference) && + SameStaticReference(modelMember.MmsReference, liveMember.MmsReference); + var userMatches = + !string.IsNullOrWhiteSpace(modelMember.UserReference) && + !string.IsNullOrWhiteSpace(liveMember.UserReference) && + SameStaticReference(modelMember.UserReference, liveMember.UserReference); + + if (mmsMatches || userMatches) + continue; + + mismatch = + $"member[{index}] model={modelMember.UserReference} ({modelMember.MmsReference}), " + + $"live={liveMember.UserReference} ({liveMember.MmsReference})"; + return false; + } + + return true; + } + private static int StaticRcbAvailabilityRank(ArMms.MmsRcbOperationalAvailability availability) => availability switch { From c53c2804e753b33839b5ed97158088e46a8ce534 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 19 Sep 2026 20:23:38 +0700 Subject: [PATCH 03/18] chore(engine): pin source-neutral static activation candidate --- engines/ARIEC61850.lock.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/engines/ARIEC61850.lock.json b/engines/ARIEC61850.lock.json index 360e72a96..c761c87e0 100644 --- a/engines/ARIEC61850.lock.json +++ b/engines/ARIEC61850.lock.json @@ -1,10 +1,10 @@ { "schemaVersion": 1, "repository": "masarray/ARIEC61850", - "ref": "main", - "commit": "648124097621046f5f127ceb1cf853fea54db730", - "sourcePullRequest": 135, - "purpose": "R7 physical SCL repair pin on the complete PR #134 smart-discovery performance head. Accepted remote AP-title/AE/PSEL/SSEL remain decoded from the exact association request bytes accepted by the IED and TSEL remains bound to the accepted COTP destination selector. Physical AA1E1F06R4 evidence additionally proved that indexed RCB siblings may differ in writable/current BufTm, IntgPd, TrgOps and OptFlds while still representing one logical SCL ReportControl; engine head 648124097621046f5f127ceb1cf853fea54db730 preserves DataSet/ConfRev/domain/LN/buffered identity while allowing that runtime-mutable setting drift. ARSAS consumer-side reconnect must reproduce the proven native calling identity byte-for-byte and canonical save uses full-model SCL with prefixed-LN identity, CDC-aware WYE/DEL/SEQ SDO and FC ownership, plus exact standard TCTR/TVTR/LTIM/EEName/MltLev CDC authority and Edition-2-only LTRK service-tracking CDCs with Edition-1 schema downgrade protection; physical R8 reuse evidence additionally requires exact MMS TypeSpecification declaration order through canonical model/SCL reload, treats SG/SE as setting data rather than control blocks, and maps MHAI THD phase groups as WYE/CMV; safety is enforced by bounded FC-read policy rather than deleting discovered model leaves. Production promotion remains fail-closed and still requires fresh physical authority. R9 reuse lock additionally preserves rptID-backed preallocated singleton RCB indexing, case-distinct canonical instance values, and compile-safe logical RCB projection; unresolved 46 trusted-SCL projection errors remain a fail-open diagnostic gap but a fail-closed promotion gap until physical evidence reaches zero. P1 trusted-SCL projection repair removes cross-DO positional dependence for multi-DO CF structures: SCL LNodeType order is not treated as MMS FC-structure order, so CF hydration is split into exact DO-scoped structured Reads and remains batched/bounded. P0 structural discovery remains unchanged. P2 makes instance-value identity exact-case end-to-end: ARSAS trusted-SCL caching uses StringComparer.Ordinal and reports projectedUniqueValues/cacheLoss; engine TypeSpecification member resolution and canonical DO/DA instance-value targeting are case-sensitive so legal paths such as tracking t/T cannot collapse or cross-resolve. R10 physical acceptance passed on AA1E1F06R4: Ed2 and Ed1 both reached projectionErrors=0 and cacheLoss=0, all planned reads succeeded, 58/58 runtime points were report-backed, and actual InformationReport traffic was observed. The exact tested commit 9935d6902d786cc69b299260fe36b835944d5e81 and merged main commit 648124097621046f5f127ceb1cf853fea54db730 have the identical source tree 1cf7e08f333f24994625e8fe8416dbd0a16195b1.", + "ref": "fix/canonical-static-report-activation", + "commit": "c559809e0a7bc58f3e91e5e298b03cdf46f660e7", + "sourcePullRequest": 137, + "purpose": "R7 physical SCL repair pin on the complete PR #134 smart-discovery performance head. Accepted remote AP-title/AE/PSEL/SSEL remain decoded from the exact association request bytes accepted by the IED and TSEL remains bound to the accepted COTP destination selector. Physical AA1E1F06R4 evidence additionally proved that indexed RCB siblings may differ in writable/current BufTm, IntgPd, TrgOps and OptFlds while still representing one logical SCL ReportControl; engine head 648124097621046f5f127ceb1cf853fea54db730 preserves DataSet/ConfRev/domain/LN/buffered identity while allowing that runtime-mutable setting drift. ARSAS consumer-side reconnect must reproduce the proven native calling identity byte-for-byte and canonical save uses full-model SCL with prefixed-LN identity, CDC-aware WYE/DEL/SEQ SDO and FC ownership, plus exact standard TCTR/TVTR/LTIM/EEName/MltLev CDC authority and Edition-2-only LTRK service-tracking CDCs with Edition-1 schema downgrade protection; physical R8 reuse evidence additionally requires exact MMS TypeSpecification declaration order through canonical model/SCL reload, treats SG/SE as setting data rather than control blocks, and maps MHAI THD phase groups as WYE/CMV; safety is enforced by bounded FC-read policy rather than deleting discovered model leaves. Production promotion remains fail-closed and still requires fresh physical authority. R9 reuse lock additionally preserves rptID-backed preallocated singleton RCB indexing, case-distinct canonical instance values, and compile-safe logical RCB projection; unresolved 46 trusted-SCL projection errors remain a fail-open diagnostic gap but a fail-closed promotion gap until physical evidence reaches zero. P1 trusted-SCL projection repair removes cross-DO positional dependence for multi-DO CF structures: SCL LNodeType order is not treated as MMS FC-structure order, so CF hydration is split into exact DO-scoped structured Reads and remains batched/bounded. P0 structural discovery remains unchanged. P2 makes instance-value identity exact-case end-to-end: ARSAS trusted-SCL caching uses StringComparer.Ordinal and reports projectedUniqueValues/cacheLoss; engine TypeSpecification member resolution and canonical DO/DA instance-value targeting are case-sensitive so legal paths such as tracking t/T cannot collapse or cross-resolve. R10 physical acceptance passed on AA1E1F06R4: Ed2 and Ed1 both reached projectionErrors=0 and cacheLoss=0, all planned reads succeeded, 58/58 runtime points were report-backed, and actual InformationReport traffic was observed. The exact tested commit 9935d6902d786cc69b299260fe36b835944d5e81 and merged main commit 648124097621046f5f127ceb1cf853fea54db730 have the identical source tree 1cf7e08f333f24994625e8fe8416dbd0a16195b1. Candidate live-discovery static-parity repair pins PR #137 only to expose the same configured-static activation sequence through a source-neutral API; this candidate is not physical acceptance authority and must not replace the R10 physical baseline until retested.", "previousTrialPin": { "commit": "4467124775d8d9d76f3db194f9fbfd97144767a8", "sourcePullRequest": 134, From 167e4babed28e781d90fb5febf5a08ae426913ca Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 19 Sep 2026 20:23:45 +0700 Subject: [PATCH 04/18] fix(discovery): route completed model through shared signal-source chooser --- MainWindow.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 69eae95b6..2020c6e4a 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -766,7 +766,7 @@ private async Task ConnectAndConfigureDeviceAsync( if (openWizard && device.SignalCount > 0) { if ((selectDevice || ReferenceEquals(SelectedDevice, device)) && !_signalSelectionWizardOpen) - await OpenSignalSelectionWizardAsync(device, restoredCount); + await OpenIedWorkspaceActionsAsync(device); else SetStatus($"{device.Name}: discovery complete. Use the edit icon on its IED card to review {restoredCount} restored selection(s)."); } From eeff774ce2878d2bad0172ede735b0ffec2494c2 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 19 Sep 2026 20:23:56 +0700 Subject: [PATCH 05/18] ux(discovery): label shared post-model signal sources --- SclSignalSelectionModeWindow.xaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SclSignalSelectionModeWindow.xaml b/SclSignalSelectionModeWindow.xaml index f4668d997..573a3c11d 100644 --- a/SclSignalSelectionModeWindow.xaml +++ b/SclSignalSelectionModeWindow.xaml @@ -52,14 +52,14 @@ From ef0cce7e0a2cadd70086c4e53709c632b8b33bb9 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 19 Sep 2026 20:23:59 +0700 Subject: [PATCH 06/18] ux(discovery): label shared post-model signal sources --- IedCardActionsGearPolicy.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IedCardActionsGearPolicy.cs b/IedCardActionsGearPolicy.cs index 0fbed72d3..e15ea5ec9 100644 --- a/IedCardActionsGearPolicy.cs +++ b/IedCardActionsGearPolicy.cs @@ -11,13 +11,13 @@ namespace ArIED61850Tester; /// /// Adds a dedicated Lucide-style gear button to every IED card. The existing pencil /// remains the direct Edit Signals shortcut; the gear is the explicit entry point for -/// the reusable IED Actions chooser (Static DataSet, Select Signals, RCB Engineering, +/// the reusable IED Actions chooser (DataSet Signals, Signal Catalog, RCB Engineering, /// COMTRADE and Browse Offline). /// internal static class IedCardActionsGearPolicy { private const string GearUid = "ARSAS.IedActionsGear"; - private const string GearToolTip = "IED Actions — Static DataSet, Select Signals, RCB Engineering, COMTRADE, Browse Offline"; + private const string GearToolTip = "IED Actions — DataSet Signals, Signal Catalog, RCB Engineering, COMTRADE, Browse Offline"; private const double GearOpticalSize = 16d; // Exact Lucide Settings outline supplied by the product owner, translated from the From a69aaa9be0679362ca33dafc089505067568852e Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 19 Sep 2026 20:24:01 +0700 Subject: [PATCH 07/18] ux(discovery): label shared post-model signal sources --- IedWorkspaceActionsCardBehavior.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IedWorkspaceActionsCardBehavior.cs b/IedWorkspaceActionsCardBehavior.cs index fb557b7f0..a93b8950d 100644 --- a/IedWorkspaceActionsCardBehavior.cs +++ b/IedWorkspaceActionsCardBehavior.cs @@ -14,7 +14,7 @@ namespace ArIED61850Tester; internal static class IedWorkspaceActionsCardBehavior { private const string LegacyToolTip = "Configure Signals"; - private const string ActionsToolTip = "IED Actions — Static DataSet, Select Signals, RCB Engineering, COMTRADE, Browse Offline"; + private const string ActionsToolTip = "IED Actions — DataSet Signals, Signal Catalog, RCB Engineering, COMTRADE, Browse Offline"; [ModuleInitializer] internal static void Initialize() From 37f3c7bea0f76d1e962e134b2e062f5efd979caf Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 19 Sep 2026 20:24:26 +0700 Subject: [PATCH 08/18] fix(reporting): keep SCL-first projection authority precedence --- Services/Iec61850MonitorRuntime.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Services/Iec61850MonitorRuntime.cs b/Services/Iec61850MonitorRuntime.cs index cb5a7d36b..1abc8af02 100644 --- a/Services/Iec61850MonitorRuntime.cs +++ b/Services/Iec61850MonitorRuntime.cs @@ -1136,7 +1136,7 @@ private async Task ReceiveReportSlicesAsync(DeviceSession session, CancellationT { var effectiveUpdates = session.StaticDataSetReportOnly ? session.StaticReportProjection.Project( - session.Device.LiveDiscoveryModel ?? session.Device.SclWorkspace?.DesignModel, + session.Device.SclWorkspace?.DesignModel ?? session.Device.LiveDiscoveryModel, session.Points.Values.ToArray(), sourceUpdate) : new[] { sourceUpdate }; From ccfdd029adeff8cf85a9fa727cd59e9f2cec73ec Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 19 Sep 2026 20:24:45 +0700 Subject: [PATCH 09/18] test(reporting): lock canonical live static DataSet authority --- ...ministicStaticReportPathRegressionTests.cs | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/tests/ARSAS.Tests/DeterministicStaticReportPathRegressionTests.cs b/tests/ARSAS.Tests/DeterministicStaticReportPathRegressionTests.cs index 4096169c8..ad84dc30e 100644 --- a/tests/ARSAS.Tests/DeterministicStaticReportPathRegressionTests.cs +++ b/tests/ARSAS.Tests/DeterministicStaticReportPathRegressionTests.cs @@ -11,10 +11,13 @@ public void StaticPath_BypassesAdaptiveHybridPlannerAndPolling() 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("GetDataSetDirectoriesAsync", source, StringComparison.Ordinal); + Assert.Contains("BuildModelDataSetDirectories", source, StringComparison.Ordinal); + Assert.Contains("discovery.DataSetDirectories", source, StringComparison.Ordinal); + Assert.Contains("TryVerifyStaticDataSetMemberOrder", 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.DoesNotContain("StartPersistentReportMonitorClientCompatibleAsync", source, StringComparison.Ordinal); Assert.Contains("PollingPointKeys = Array.Empty()", source, StringComparison.Ordinal); Assert.Contains("PollingFallbackSignalCount = 0", source, StringComparison.Ordinal); @@ -44,21 +47,36 @@ public void TrustedSclStaticPath_RequestsOneShotGi_WhileSafeTrialRemainsReadOnly } [Fact] - public void StaticPath_RequiresConfiguredRcbFamilyAndOrderedLiveDataSetDirectory() + public void StaticPath_RequiresConfiguredRcbFamilyAndCanonicalOrderVerifiedByDiscoverySnapshot() { 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("live DataSet directory could not prove an ordered non-empty member list", source, StringComparison.Ordinal); - Assert.Contains("directory.Members", source, StringComparison.Ordinal); + Assert.Contains("canonical model has no ordered DataSet member list", source, StringComparison.Ordinal); + Assert.Contains("discovery snapshot has no ordered live 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); } + [Fact] + public void StaticPath_DoesNotIssueSecondDataSetDirectoryReadOrUseRawLiveMembersAsProjectionAuthority() + { + var source = Read("Services/NativeIec61850Client.StaticDataSetReporting.cs"); + + Assert.Contains("discovery.DataSetDirectories.SingleOrDefault", source, StringComparison.Ordinal); + Assert.Contains("Members = modelDirectory.Members", source, StringComparison.Ordinal); + Assert.DoesNotContain("GetDataSetDirectoriesAsync", source, StringComparison.Ordinal); + Assert.DoesNotContain("Members = liveDirectory.Members", source, StringComparison.Ordinal); + Assert.Contains("member[{index}]", source, StringComparison.Ordinal); + } + [Fact] public void StaticPath_SclAuthorityCannotBeDisplacedByLivePeerRcb() { From 3da726f213cccd4c31d0ebe4f183938f18600d35 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 19 Sep 2026 20:25:12 +0700 Subject: [PATCH 10/18] test(discovery): lock canonical static parity routing --- ...iveDiscoveryStaticParityRegressionTests.cs | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 tests/ARSAS.Tests/LiveDiscoveryStaticParityRegressionTests.cs diff --git a/tests/ARSAS.Tests/LiveDiscoveryStaticParityRegressionTests.cs b/tests/ARSAS.Tests/LiveDiscoveryStaticParityRegressionTests.cs new file mode 100644 index 000000000..4cfd44d10 --- /dev/null +++ b/tests/ARSAS.Tests/LiveDiscoveryStaticParityRegressionTests.cs @@ -0,0 +1,78 @@ +namespace ARSAS.Tests; + +public sealed class LiveDiscoveryStaticParityRegressionTests +{ + [Fact] + public void Discovery_EntersSharedPostModelChooser() + { + var source = Read("MainWindow.xaml.cs"); + var start = source.IndexOf( + "private async Task ConnectAndConfigureDeviceAsync", + StringComparison.Ordinal); + var end = source.IndexOf( + "private async Task ConnectUsingSavedModelAsync", + start, + StringComparison.Ordinal); + Assert.True(start >= 0 && end > start); + + var discovery = source[start..end]; + Assert.Contains("await OpenIedWorkspaceActionsAsync(device);", discovery, StringComparison.Ordinal); + Assert.DoesNotContain("OpenSignalSelectionWizardAsync(device, restoredCount)", discovery, StringComparison.Ordinal); + } + + [Fact] + public void DataSetSignals_UsesCanonicalMembersAndConfiguredStaticActivation() + { + var staticPath = Read("Services/NativeIec61850Client.StaticDataSetReporting.cs"); + + Assert.Contains("BuildModelDataSetDirectories", staticPath, StringComparison.Ordinal); + Assert.Contains("Members = modelDirectory.Members", staticPath, StringComparison.Ordinal); + Assert.Contains("discovery.DataSetDirectories", staticPath, StringComparison.Ordinal); + Assert.Contains("TryVerifyStaticDataSetMemberOrder", staticPath, StringComparison.Ordinal); + Assert.Contains("StartConfiguredStaticReportMonitorAsync", staticPath, StringComparison.Ordinal); + Assert.DoesNotContain("GetDataSetDirectoriesAsync", staticPath, StringComparison.Ordinal); + Assert.DoesNotContain("StartPersistentReportMonitorClientCompatibleAsync", staticPath, StringComparison.Ordinal); + } + + [Fact] + public void StaticProjection_UsesSameAuthorityPrecedenceAsPlanning() + { + var runtime = Read("Services/Iec61850MonitorRuntime.cs"); + + Assert.Contains( + "session.Device.SclWorkspace?.DesignModel ?? session.Device.LiveDiscoveryModel", + runtime, + StringComparison.Ordinal); + Assert.DoesNotContain( + "session.Device.LiveDiscoveryModel ?? session.Device.SclWorkspace?.DesignModel", + runtime, + StringComparison.Ordinal); + } + + [Fact] + public void Chooser_UsesSourceNeutralLabels() + { + var xaml = Read("SclSignalSelectionModeWindow.xaml"); + + Assert.Contains("DataSet Signals", xaml, StringComparison.Ordinal); + Assert.Contains("Static report-only", xaml, StringComparison.Ordinal); + Assert.Contains("Signal Catalog", xaml, StringComparison.Ordinal); + } + + 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 != null) + { + var candidate = Path.Combine(directory.FullName, relativePath); + if (File.Exists(candidate)) + return candidate; + directory = directory.Parent; + } + + throw new FileNotFoundException(relativePath); + } +} From 895f5c7a9ab410ade975b8ed209077fe1cd6356e Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 19 Sep 2026 20:26:12 +0700 Subject: [PATCH 11/18] chore(engine): advance static activation candidate pin --- engines/ARIEC61850.lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engines/ARIEC61850.lock.json b/engines/ARIEC61850.lock.json index c761c87e0..38b24c4c6 100644 --- a/engines/ARIEC61850.lock.json +++ b/engines/ARIEC61850.lock.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "repository": "masarray/ARIEC61850", "ref": "fix/canonical-static-report-activation", - "commit": "c559809e0a7bc58f3e91e5e298b03cdf46f660e7", + "commit": "375543da77ea515757407e7587e08a259df9653a", "sourcePullRequest": 137, "purpose": "R7 physical SCL repair pin on the complete PR #134 smart-discovery performance head. Accepted remote AP-title/AE/PSEL/SSEL remain decoded from the exact association request bytes accepted by the IED and TSEL remains bound to the accepted COTP destination selector. Physical AA1E1F06R4 evidence additionally proved that indexed RCB siblings may differ in writable/current BufTm, IntgPd, TrgOps and OptFlds while still representing one logical SCL ReportControl; engine head 648124097621046f5f127ceb1cf853fea54db730 preserves DataSet/ConfRev/domain/LN/buffered identity while allowing that runtime-mutable setting drift. ARSAS consumer-side reconnect must reproduce the proven native calling identity byte-for-byte and canonical save uses full-model SCL with prefixed-LN identity, CDC-aware WYE/DEL/SEQ SDO and FC ownership, plus exact standard TCTR/TVTR/LTIM/EEName/MltLev CDC authority and Edition-2-only LTRK service-tracking CDCs with Edition-1 schema downgrade protection; physical R8 reuse evidence additionally requires exact MMS TypeSpecification declaration order through canonical model/SCL reload, treats SG/SE as setting data rather than control blocks, and maps MHAI THD phase groups as WYE/CMV; safety is enforced by bounded FC-read policy rather than deleting discovered model leaves. Production promotion remains fail-closed and still requires fresh physical authority. R9 reuse lock additionally preserves rptID-backed preallocated singleton RCB indexing, case-distinct canonical instance values, and compile-safe logical RCB projection; unresolved 46 trusted-SCL projection errors remain a fail-open diagnostic gap but a fail-closed promotion gap until physical evidence reaches zero. P1 trusted-SCL projection repair removes cross-DO positional dependence for multi-DO CF structures: SCL LNodeType order is not treated as MMS FC-structure order, so CF hydration is split into exact DO-scoped structured Reads and remains batched/bounded. P0 structural discovery remains unchanged. P2 makes instance-value identity exact-case end-to-end: ARSAS trusted-SCL caching uses StringComparer.Ordinal and reports projectedUniqueValues/cacheLoss; engine TypeSpecification member resolution and canonical DO/DA instance-value targeting are case-sensitive so legal paths such as tracking t/T cannot collapse or cross-resolve. R10 physical acceptance passed on AA1E1F06R4: Ed2 and Ed1 both reached projectionErrors=0 and cacheLoss=0, all planned reads succeeded, 58/58 runtime points were report-backed, and actual InformationReport traffic was observed. The exact tested commit 9935d6902d786cc69b299260fe36b835944d5e81 and merged main commit 648124097621046f5f127ceb1cf853fea54db730 have the identical source tree 1cf7e08f333f24994625e8fe8416dbd0a16195b1. Candidate live-discovery static-parity repair pins PR #137 only to expose the same configured-static activation sequence through a source-neutral API; this candidate is not physical acceptance authority and must not replace the R10 physical baseline until retested.", "previousTrialPin": { From eb3a431c38ea1ba41d16cc797926f3bf22771b81 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 19 Sep 2026 20:27:06 +0700 Subject: [PATCH 12/18] fix(ci): keep immutable engine lock schema while pinning candidate commit --- engines/ARIEC61850.lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engines/ARIEC61850.lock.json b/engines/ARIEC61850.lock.json index 38b24c4c6..a99f5514c 100644 --- a/engines/ARIEC61850.lock.json +++ b/engines/ARIEC61850.lock.json @@ -1,7 +1,7 @@ { "schemaVersion": 1, "repository": "masarray/ARIEC61850", - "ref": "fix/canonical-static-report-activation", + "ref": "main", "commit": "375543da77ea515757407e7587e08a259df9653a", "sourcePullRequest": 137, "purpose": "R7 physical SCL repair pin on the complete PR #134 smart-discovery performance head. Accepted remote AP-title/AE/PSEL/SSEL remain decoded from the exact association request bytes accepted by the IED and TSEL remains bound to the accepted COTP destination selector. Physical AA1E1F06R4 evidence additionally proved that indexed RCB siblings may differ in writable/current BufTm, IntgPd, TrgOps and OptFlds while still representing one logical SCL ReportControl; engine head 648124097621046f5f127ceb1cf853fea54db730 preserves DataSet/ConfRev/domain/LN/buffered identity while allowing that runtime-mutable setting drift. ARSAS consumer-side reconnect must reproduce the proven native calling identity byte-for-byte and canonical save uses full-model SCL with prefixed-LN identity, CDC-aware WYE/DEL/SEQ SDO and FC ownership, plus exact standard TCTR/TVTR/LTIM/EEName/MltLev CDC authority and Edition-2-only LTRK service-tracking CDCs with Edition-1 schema downgrade protection; physical R8 reuse evidence additionally requires exact MMS TypeSpecification declaration order through canonical model/SCL reload, treats SG/SE as setting data rather than control blocks, and maps MHAI THD phase groups as WYE/CMV; safety is enforced by bounded FC-read policy rather than deleting discovered model leaves. Production promotion remains fail-closed and still requires fresh physical authority. R9 reuse lock additionally preserves rptID-backed preallocated singleton RCB indexing, case-distinct canonical instance values, and compile-safe logical RCB projection; unresolved 46 trusted-SCL projection errors remain a fail-open diagnostic gap but a fail-closed promotion gap until physical evidence reaches zero. P1 trusted-SCL projection repair removes cross-DO positional dependence for multi-DO CF structures: SCL LNodeType order is not treated as MMS FC-structure order, so CF hydration is split into exact DO-scoped structured Reads and remains batched/bounded. P0 structural discovery remains unchanged. P2 makes instance-value identity exact-case end-to-end: ARSAS trusted-SCL caching uses StringComparer.Ordinal and reports projectedUniqueValues/cacheLoss; engine TypeSpecification member resolution and canonical DO/DA instance-value targeting are case-sensitive so legal paths such as tracking t/T cannot collapse or cross-resolve. R10 physical acceptance passed on AA1E1F06R4: Ed2 and Ed1 both reached projectionErrors=0 and cacheLoss=0, all planned reads succeeded, 58/58 runtime points were report-backed, and actual InformationReport traffic was observed. The exact tested commit 9935d6902d786cc69b299260fe36b835944d5e81 and merged main commit 648124097621046f5f127ceb1cf853fea54db730 have the identical source tree 1cf7e08f333f24994625e8fe8416dbd0a16195b1. Candidate live-discovery static-parity repair pins PR #137 only to expose the same configured-static activation sequence through a source-neutral API; this candidate is not physical acceptance authority and must not replace the R10 physical baseline until retested.", From e050cd78d27e7f2a3c78f73f12eef0c2f617b5c0 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 19 Sep 2026 20:28:21 +0700 Subject: [PATCH 13/18] diag(dataset): list exact unresolved primary memberships --- .../Iec61850DataSetCompletenessDiagnostic.cs | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/Services/Iec61850DataSetCompletenessDiagnostic.cs b/Services/Iec61850DataSetCompletenessDiagnostic.cs index 9b44624a1..1bf59446f 100644 --- a/Services/Iec61850DataSetCompletenessDiagnostic.cs +++ b/Services/Iec61850DataSetCompletenessDiagnostic.cs @@ -24,6 +24,8 @@ public sealed record Iec61850DataSetCompletenessSnapshot( public bool IsComplete => StaticMemberCount == RepresentedCount && MissingCount == 0; public IReadOnlyList DataSets { get; init; } = Array.Empty(); + public IReadOnlyList PrimaryUnresolvedReferences { get; init; } + = Array.Empty(); public string Summary => $"DataSets={DataSetCount:N0}; static members={StaticMemberCount:N0}; semantic descriptors={MandatoryInventoryCount:N0}; " + @@ -89,15 +91,28 @@ public static Iec61850DataSetCompletenessSnapshot Evaluate( dataSetMissing)); } + var unresolvedPrimaryReferences = mandatory + .Where(descriptor => descriptor.ResolutionStatus == Iec61850SignalCatalogResolutionStatus.Unresolved) + .SelectMany(descriptor => descriptor.DataSetMemberships.Count > 0 + ? descriptor.DataSetMemberships.Select(membership => + $"{membership.DataSetReference}[{membership.MemberIndex}] -> " + + $"{FirstNonEmpty(membership.CanonicalMemberReference, membership.OriginalMemberReference, descriptor.DesignReference)}") + : new[] { FirstNonEmpty(descriptor.DesignReference, descriptor.ObservedReference, "") }) + .Where(reference => !string.IsNullOrWhiteSpace(reference)) + .Distinct(StringComparer.Ordinal) + .OrderBy(reference => reference, StringComparer.Ordinal) + .ToArray(); + return new Iec61850DataSetCompletenessSnapshot( model.DataSets.Count, staticMemberCount, mandatory.Count, represented, - mandatory.Count(descriptor => descriptor.ResolutionStatus == Iec61850SignalCatalogResolutionStatus.Unresolved), + unresolvedPrimaryReferences.Length, missing) { - DataSets = dataSetSnapshots + DataSets = dataSetSnapshots, + PrimaryUnresolvedReferences = unresolvedPrimaryReferences }; } @@ -117,6 +132,11 @@ public static IEnumerable FormatReportLines(Iec61850DataSetCompletenessS yield return $" {dataSet.Reference}: {dataSet.RepresentedCount:N0}/{dataSet.StaticMemberCount:N0} represented • {dataSet.MissingCount:N0} missing"; } + foreach (var reference in snapshot.PrimaryUnresolvedReferences.Take(16)) + yield return $" UNRESOLVED PRIMARY : {reference}"; + if (snapshot.PrimaryUnresolvedReferences.Count > 16) + yield return $" ... unresolved : {snapshot.PrimaryUnresolvedReferences.Count - 16:N0} more member(s)"; + if (snapshot.MissingCount == 0 || maxMissing <= 0) yield break; @@ -146,6 +166,9 @@ public static IEnumerable FormatReportLines(Iec61850DataSetCompletenessS yield return $" ... : {snapshot.MissingCount - emitted:N0} more missing member(s)"; } + private static string FirstNonEmpty(params string?[] values) + => values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value))?.Trim() ?? string.Empty; + private static string Literal(string? reference) => (reference ?? string.Empty).Trim(); } From a6b129d37755922d9d5635c36897c9c7a6b9bbb8 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 19 Sep 2026 20:30:14 +0700 Subject: [PATCH 14/18] fix(discovery): promote physically proven smart route into main source --- Services/NativeIec61850Client.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Services/NativeIec61850Client.cs b/Services/NativeIec61850Client.cs index 04a657e42..91cc9cf00 100644 --- a/Services/NativeIec61850Client.cs +++ b/Services/NativeIec61850Client.cs @@ -62,6 +62,7 @@ public async Task ConnectAsync(string ipAddress, int port, CancellationToken can LastConnectionTechnicalSummary = string.Empty; _lastDiscovery = null; _liveModel = null; + ResetSmartDiscoveryAuthority(); // __P0_5C_CONNECT_RESET__ ClearCanonicalModel(); _reportMonitorSessions.Clear(); _reportMonitorCoverage.Clear(); @@ -127,6 +128,9 @@ public Iec61850DeviceDiagnosticSnapshot CaptureDiagnosticSnapshot( public async Task> DiscoverSignalsAsync(CancellationToken cancellationToken, IProgress? progress = null) { + if (SmartDiscoveryCaptureModeEnabled) + return await DiscoverSignalsSmartForCaptureAsync(cancellationToken, progress).ConfigureAwait(false); + LastDiscoverySummary = string.Empty; cancellationToken.ThrowIfCancellationRequested(); progress?.Report(new IedDiscoveryProgress( @@ -1950,6 +1954,8 @@ private static Iec61850ControlCommandResult ControlWireUnknownFailure( public async ValueTask DisposeAsync() { + ResetSmartDiscoveryAuthority(); // __P0_5C_DISPOSE_RESET__ + await DisposeControlSessionsAsync().ConfigureAwait(false); await StopReportMonitorsAsync().ConfigureAwait(false); await _mmsIoGate.WaitAsync().ConfigureAwait(false); @@ -1979,7 +1985,9 @@ public async ValueTask DisposeAsync() var service = new ArControl.Iec61850ControlService(); var opened = await RunMmsOperationAsync( - () => service.OpenAsync(_session, signal.ObjectReference, cancellationToken), + () => _lastDiscovery != null + ? service.OpenAsync(_session, signal.ObjectReference, _lastDiscovery.Snapshot.DomainVariables, cancellationToken) + : service.OpenAsync(_session, signal.ObjectReference, cancellationToken), cancellationToken).ConfigureAwait(false); _controlSessions[key] = opened; return opened; From 63fb3a61a09596cc9031c962ab7157c72b98748f Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 19 Sep 2026 20:31:04 +0700 Subject: [PATCH 15/18] test(discovery): lock physically proven smart route in source --- .../SmartDiscoveryAssociationSingleFlightRegressionTests.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/ARSAS.Tests/SmartDiscoveryAssociationSingleFlightRegressionTests.cs b/tests/ARSAS.Tests/SmartDiscoveryAssociationSingleFlightRegressionTests.cs index 0e002ac18..5bba3cb36 100644 --- a/tests/ARSAS.Tests/SmartDiscoveryAssociationSingleFlightRegressionTests.cs +++ b/tests/ARSAS.Tests/SmartDiscoveryAssociationSingleFlightRegressionTests.cs @@ -32,6 +32,7 @@ public void P05c_ReconnectAndDispose_InvalidateGenerationAndBlockStalePublish() var lifecycle = File.ReadAllText(FindRepoFile("Services/NativeIec61850Client.SmartDiscoveryLifecycle.cs")); var capture = File.ReadAllText(FindRepoFile("Services/NativeIec61850Client.SmartDiscoveryCapture.cs")); var patcher = File.ReadAllText(FindRepoFile("scripts/enable-smart-discovery-capture.ps1")); + var native = File.ReadAllText(FindRepoFile("Services/NativeIec61850Client.cs")); Assert.Contains("_smartDiscoveryAssociationGeneration++", lifecycle, StringComparison.Ordinal); Assert.Contains("generation != _smartDiscoveryAssociationGeneration", lifecycle, StringComparison.Ordinal); @@ -40,6 +41,10 @@ public void P05c_ReconnectAndDispose_InvalidateGenerationAndBlockStalePublish() Assert.Contains("__P0_5C_CONNECT_RESET__", patcher, StringComparison.Ordinal); Assert.Contains("__P0_5C_DISPOSE_RESET__", patcher, StringComparison.Ordinal); Assert.Contains("ResetSmartDiscoveryAuthority(); // __P0_5C_DISPOSE_RESET__", patcher, StringComparison.Ordinal); + Assert.Contains("return await DiscoverSignalsSmartForCaptureAsync(cancellationToken, progress)", native, StringComparison.Ordinal); + Assert.Contains("ResetSmartDiscoveryAuthority(); // __P0_5C_CONNECT_RESET__", native, StringComparison.Ordinal); + Assert.Contains("ResetSmartDiscoveryAuthority(); // __P0_5C_DISPOSE_RESET__", native, StringComparison.Ordinal); + Assert.Contains("_lastDiscovery.Snapshot.DomainVariables", native, StringComparison.Ordinal); } [Fact] From 92603e912f39161b2d3f503768eae47657cfbadd Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 19 Sep 2026 20:31:18 +0700 Subject: [PATCH 16/18] evidence(discovery): record effective R10 build transform provenance --- evidence/iedscout-convergence-target.json | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/evidence/iedscout-convergence-target.json b/evidence/iedscout-convergence-target.json index a853fd0ea..fee68fa87 100644 --- a/evidence/iedscout-convergence-target.json +++ b/evidence/iedscout-convergence-target.json @@ -29,7 +29,7 @@ "repository": "masarray/arsas", "pullRequest": 324, "branch": "test/smart-ied-discovery-pr134", - "status": "physical-proven-merge-ready" + "status": "physical-proven-effective-field-build; source-route-promotion-candidate" } }, "physicalReference": { @@ -132,7 +132,7 @@ "mergeAllowedAfterPhysicalRetest": true, "physicalRetestPassed": true, "physicalRetestDate": "2026-09-19", - "note": "R10 physical discovery/save/reopen evidence passed for the exact ARSAS/engine source tree. Merge is allowed; production promotion remains a separate release decision." + "note": "R10 physically proved the effective smart-discovery field build plus the pinned engine tree. The field-build workflow injected the smart discovery entry route at build time; the unmodified ARSAS source tree did not contain that route. Production/mainline authority therefore requires promoting the proven route into source and repeating physical acceptance on the resulting normal build." }, "legacyPolicy": { "rule": "Historical PRs may remain as provenance only; they must not be used as new branch bases after an active convergence authority supersedes them.", @@ -204,7 +204,11 @@ "appHead": "eb8eb13d491f9aa265205852b8a4bab07af440ff", "engineTestedHead": "9935d6902d786cc69b299260fe36b835944d5e81", "engineMergedMain": "648124097621046f5f127ceb1cf853fea54db730", - "engineTree": "1cf7e08f333f24994625e8fe8416dbd0a16195b1" + "engineTree": "1cf7e08f333f24994625e8fe8416dbd0a16195b1", + "buildTransformScript": "scripts/enable-smart-discovery-capture.ps1", + "effectiveDiscoveryEntrypoint": "DiscoverSignalsAsync -> DiscoverSignalsSmartForCaptureAsync", + "unmodifiedSourceContainedSmartRoute": false, + "provenanceCorrection": "The app commit identifies the repository revision, but the physically tested field artifact also included the workflow build transform above. Commit/tree equality alone is not sufficient to identify equivalent discovery behavior." }, "discovery": { "associations": 1, From d9b9a900e4074fb2746c6b8789e4f72f24c213cd Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 19 Sep 2026 20:33:07 +0700 Subject: [PATCH 17/18] chore(public): neutralize release-note interoperability wording --- landing/release-notes.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/landing/release-notes.json b/landing/release-notes.json index 0b00ab1aa..75024309f 100644 --- a/landing/release-notes.json +++ b/landing/release-notes.json @@ -8,13 +8,13 @@ "summaryId": "ARSAS 1.6.38 adalah stable release Windows terverifikasi terbaru. Identitas paket, link download, checksum, dan publication evidence disinkronkan otomatis dari GitHub Release bertag.", "highlights": [ "Polish WPF typography with embedded Inter and smooth rendering", - "convergence(discovery/scl): IEDScout-parity discovery and usable SCL", + "convergence(discovery/scl): reference-parity discovery and usable SCL", "fix(ci): restore R10 post-merge release gates", "The verified Windows installer is published as the stable ARSAS 1.6.38 package." ], "highlightsId": [ "Perubahan rilis: Polish WPF typography with embedded Inter and smooth rendering", - "Perubahan rilis: convergence(discovery/scl): IEDScout-parity discovery and usable SCL", + "Perubahan rilis: convergence(discovery/scl): reference-parity discovery and usable SCL", "Perubahan rilis: fix(ci): restore R10 post-merge release gates", "Installer Windows terverifikasi dipublikasikan sebagai paket stabil ARSAS 1.6.38." ], From c35bc858eb319eda8475213d9dd6934e9ca9cabc Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 19 Sep 2026 20:33:53 +0700 Subject: [PATCH 18/18] evidence(discovery): require fresh physical acceptance for source promotion --- evidence/iedscout-convergence-target.json | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/evidence/iedscout-convergence-target.json b/evidence/iedscout-convergence-target.json index fee68fa87..20bcde006 100644 --- a/evidence/iedscout-convergence-target.json +++ b/evidence/iedscout-convergence-target.json @@ -119,7 +119,7 @@ "promotion": { "productionPromoted": false, "mergeAllowedBeforePhysicalRetest": false, - "physicalRetestRequired": false, + "physicalRetestRequired": true, "requiredEvidence": [ "new ARSAS PCAP", "new generated Ed2 IID", @@ -129,10 +129,11 @@ "Ed1 SCL-assisted reuse diagnostic with projectionErrorSamples", "same-relay comparison against IEDScout" ], - "mergeAllowedAfterPhysicalRetest": true, - "physicalRetestPassed": true, + "mergeAllowedAfterPhysicalRetest": false, + "physicalRetestPassed": false, "physicalRetestDate": "2026-09-19", - "note": "R10 physically proved the effective smart-discovery field build plus the pinned engine tree. The field-build workflow injected the smart discovery entry route at build time; the unmodified ARSAS source tree did not contain that route. Production/mainline authority therefore requires promoting the proven route into source and repeating physical acceptance on the resulting normal build." + "note": "Historical R10 physical evidence remains valid for the effective field artifact that included the build-time smart-route transform. This candidate promotes that transform into normal source and also changes live static-report orchestration, so fresh physical acceptance is mandatory before merge or publication.", + "candidateRetestStatus": "required-after-smart-route-source-promotion-and-live-static-parity-repair" }, "legacyPolicy": { "rule": "Historical PRs may remain as provenance only; they must not be used as new branch bases after an active convergence authority supersedes them.",