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
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()
diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs
index 69eae95b6..2a00702ee 100644
--- a/MainWindow.xaml.cs
+++ b/MainWindow.xaml.cs
@@ -766,7 +766,14 @@ private async Task ConnectAndConfigureDeviceAsync(
if (openWizard && device.SignalCount > 0)
{
if ((selectDevice || ReferenceEquals(SelectedDevice, device)) && !_signalSelectionWizardOpen)
- await OpenSignalSelectionWizardAsync(device, restoredCount);
+ // Live discovery and Open SCL are only different model-ingestion
+ // sources. Once the typed IED model exists, both must enter the same
+ // operator decision: exact DataSet Signals in deterministic static
+ // report-only mode, or the broader manual Signal Catalog in Hybrid
+ // mode. Opening the manual wizard directly here silently skipped the
+ // Static DataSet authority and left newly discovered IEDs on mixed
+ // static/dynamic reporting plus MMS polling fallback.
+ await OpenIedWorkspaceActionsAsync(device);
else
SetStatus($"{device.Name}: discovery complete. Use the edit icon on its IED card to review {restoredCount} restored selection(s).");
}
diff --git a/SclSignalSelectionModeWindow.xaml b/SclSignalSelectionModeWindow.xaml
index f4668d997..573a3c11d 100644
--- a/SclSignalSelectionModeWindow.xaml
+++ b/SclSignalSelectionModeWindow.xaml
@@ -52,14 +52,14 @@
diff --git a/docs/static-dataset-report-only-contract.md b/docs/static-dataset-report-only-contract.md
index b9df6ab8e..8489e168a 100644
--- a/docs/static-dataset-report-only-contract.md
+++ b/docs/static-dataset-report-only-contract.md
@@ -11,7 +11,7 @@ This document is the acceptance boundary for ARSAS Static DataSet monitoring.
- Dynamic DataSet writes are disabled in this mode.
- ARIEC planner fallback candidates are diagnostic evidence only and must never become silent MMS polling.
-Manual / Select Signals mode keeps the normal Hybrid acquisition behavior and is intentionally outside this contract.
+Manual / Signal Catalog mode keeps the normal Hybrid acquisition behavior and is intentionally outside this contract.
## Deterministic configured-RCB path
diff --git a/tests/ARSAS.Tests/IedCardActionsGearRegressionTests.cs b/tests/ARSAS.Tests/IedCardActionsGearRegressionTests.cs
index f7d7ff410..de542687f 100644
--- a/tests/ARSAS.Tests/IedCardActionsGearRegressionTests.cs
+++ b/tests/ARSAS.Tests/IedCardActionsGearRegressionTests.cs
@@ -8,7 +8,7 @@ public void IedCard_HasDedicatedGearEntryPointForReusableActions()
var source = File.ReadAllText(FindRepoFile("IedCardActionsGearPolicy.cs"));
Assert.Contains("GearUid = \"ARSAS.IedActionsGear\"", source, StringComparison.Ordinal);
- Assert.Contains("IED Actions — Static DataSet, Select Signals, RCB Engineering, COMTRADE, Browse Offline", source, StringComparison.Ordinal);
+ Assert.Contains("IED Actions — DataSet Signals, Signal Catalog, RCB Engineering, COMTRADE, Browse Offline", source, StringComparison.Ordinal);
Assert.Contains("OpenIedWorkspaceActionsAsync(device)", source, StringComparison.Ordinal);
Assert.Contains("actionBar.Children.Add(CreateGearButton(device))", source, StringComparison.Ordinal);
}
diff --git a/tests/ARSAS.Tests/LiveDiscoveryPostModelWorkflowRegressionTests.cs b/tests/ARSAS.Tests/LiveDiscoveryPostModelWorkflowRegressionTests.cs
new file mode 100644
index 000000000..c7021a2f5
--- /dev/null
+++ b/tests/ARSAS.Tests/LiveDiscoveryPostModelWorkflowRegressionTests.cs
@@ -0,0 +1,92 @@
+namespace ARSAS.Tests;
+
+public sealed class LiveDiscoveryPostModelWorkflowRegressionTests
+{
+ [Fact]
+ public void SuccessfulDiscovery_UsesSharedPostModelChooser_InsteadOfOpeningManualCatalogDirectly()
+ {
+ 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("device.LiveDiscoveryModel", Read("Services/Iec61850MonitorRuntime.cs"), StringComparison.Ordinal);
+ Assert.Contains("await OpenIedWorkspaceActionsAsync(device);", discovery, StringComparison.Ordinal);
+ Assert.DoesNotContain("OpenSignalSelectionWizardAsync(device, restoredCount)", discovery, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void SharedPostModelChooser_RoutesDataSetSignalsToExactStaticReportOnlyAuthority()
+ {
+ var actions = Read("MainWindow.SclQuickActions.cs");
+ var sharedAuthority = Read("MainWindow.SharedSclWorkspace.cs");
+ var staticRuntime = Read("Services/NativeIec61850Client.StaticDataSetReporting.cs");
+
+ var staticStart = actions.IndexOf("if (dialog.UseStaticDataSet)", StringComparison.Ordinal);
+ var manualStart = actions.IndexOf("var accepted = await OpenSignalSelectionWizardAsync", staticStart, StringComparison.Ordinal);
+ Assert.True(staticStart >= 0 && manualStart > staticStart);
+ var staticChoice = actions[staticStart..manualStart];
+
+ Assert.Contains("ApplyStaticDataSetSelection(device);", staticChoice, StringComparison.Ordinal);
+ Assert.Contains("await StartDeviceMonitorAsync(device)", staticChoice, StringComparison.Ordinal);
+ Assert.DoesNotContain("UseHybrid", staticChoice, StringComparison.Ordinal);
+
+ Assert.Contains("Iec61850DataSetSignalInventoryService.EnsureMandatorySignals(device)", sharedAuthority, StringComparison.Ordinal);
+ Assert.Contains("Iec61850StaticDataSetAuthoritySelection.Build(device)", sharedAuthority, StringComparison.Ordinal);
+ Assert.Contains("Iec61850MonitoringModeRegistry.UseStaticDataSetReportOnly(device)", sharedAuthority, StringComparison.Ordinal);
+
+ Assert.Contains("device.SclWorkspace?.DesignModel ?? device.LiveDiscoveryModel", staticRuntime, StringComparison.Ordinal);
+ Assert.Contains("AllowDynamicDataSetWrites = false", staticRuntime, StringComparison.Ordinal);
+ Assert.Contains("PollingPointKeys = Array.Empty()", staticRuntime, StringComparison.Ordinal);
+ Assert.Contains("PollingFallbackSignalCount = 0", staticRuntime, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void SharedPostModelChooser_RoutesSignalCatalogToHybridOnlyAfterManualSelectionIsAccepted()
+ {
+ var actions = Read("MainWindow.SclQuickActions.cs");
+ var manualStart = actions.IndexOf("var accepted = await OpenSignalSelectionWizardAsync", StringComparison.Ordinal);
+ var methodEnd = actions.IndexOf("internal async Task OpenIedWorkspaceActionsFromCardAsync", manualStart, StringComparison.Ordinal);
+ Assert.True(manualStart >= 0 && methodEnd > manualStart);
+ var manualChoice = actions[manualStart..methodEnd];
+
+ var acceptedGuard = manualChoice.IndexOf("if (!accepted)", StringComparison.Ordinal);
+ var hybridSwitch = manualChoice.IndexOf("Iec61850MonitoringModeRegistry.UseHybrid(device)", StringComparison.Ordinal);
+ Assert.True(acceptedGuard >= 0 && hybridSwitch > acceptedGuard);
+ Assert.Contains("autoStartAfterSave: false", manualChoice, StringComparison.Ordinal);
+ Assert.Contains("await StartDeviceMonitorAsync(device)", manualChoice, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void Chooser_UsesSourceNeutralSignalSourceLabels()
+ {
+ 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);
+ }
+}
diff --git a/tests/ARSAS.Tests/SclTaskFirstQuickStartRegressionTests.cs b/tests/ARSAS.Tests/SclTaskFirstQuickStartRegressionTests.cs
index 83156482e..713008514 100644
--- a/tests/ARSAS.Tests/SclTaskFirstQuickStartRegressionTests.cs
+++ b/tests/ARSAS.Tests/SclTaskFirstQuickStartRegressionTests.cs
@@ -10,8 +10,8 @@ public void QuickStart_SeparatesOfflineEngineeringFromMonitoringIntent()
Assert.Contains("ContextHeading", xaml, StringComparison.Ordinal);
Assert.Contains("Workspace opened offline", code, StringComparison.Ordinal);
- Assert.Contains("Static DataSet", xaml, StringComparison.Ordinal);
- Assert.Contains("Select Signals", xaml, StringComparison.Ordinal);
+ Assert.Contains("DataSet Signals", xaml, StringComparison.Ordinal);
+ Assert.Contains("Signal Catalog", xaml, StringComparison.Ordinal);
Assert.Contains("RCB Engineering", xaml, StringComparison.Ordinal);
Assert.Contains("Download COMTRADE", xaml, StringComparison.Ordinal);
Assert.Contains("Browse Offline", xaml, StringComparison.Ordinal);
@@ -61,7 +61,7 @@ public void IedCard_EditAction_ReopensSameChooser_AndPinsActionsToClickedIed()
var actions = File.ReadAllText(FindRepoFile("MainWindow.SclQuickActions.cs"));
var chooser = File.ReadAllText(FindRepoFile("SclSignalSelectionModeWindow.xaml.cs"));
- Assert.Contains("IED Actions — Static DataSet, Select Signals, RCB Engineering, COMTRADE, Browse Offline", behavior, StringComparison.Ordinal);
+ Assert.Contains("IED Actions — DataSet Signals, Signal Catalog, RCB Engineering, COMTRADE, Browse Offline", behavior, StringComparison.Ordinal);
Assert.Contains("args.Handled = true;", behavior, StringComparison.Ordinal);
Assert.Contains("OpenIedWorkspaceActionsFromCardAsync(button)", behavior, StringComparison.Ordinal);
Assert.Contains("new SclSignalSelectionModeWindow(1, device)", actions, StringComparison.Ordinal);
diff --git a/tests/ARSAS.Tests/WorkspaceModeSwitchTests.cs b/tests/ARSAS.Tests/WorkspaceModeSwitchTests.cs
index fd76fc1a6..efa9ad9af 100644
--- a/tests/ARSAS.Tests/WorkspaceModeSwitchTests.cs
+++ b/tests/ARSAS.Tests/WorkspaceModeSwitchTests.cs
@@ -32,8 +32,8 @@ public void SclImport_UsesOneSelectionAuthorityAcrossEngineeringAndFat()
Assert.Contains("new SclSignalSelectionModeWindow", workflow, StringComparison.Ordinal);
Assert.DoesNotContain("MessageBox.Show", workflow, StringComparison.Ordinal);
Assert.Contains("Workspace opened offline", selectionWindow, StringComparison.Ordinal);
- Assert.Contains("Static DataSet", selectionWindow, StringComparison.Ordinal);
- Assert.Contains("Select Signals", selectionWindow, StringComparison.Ordinal);
+ Assert.Contains("DataSet Signals", selectionWindow, StringComparison.Ordinal);
+ Assert.Contains("Signal Catalog", selectionWindow, StringComparison.Ordinal);
Assert.Contains("RCB Engineering", selectionWindow, StringComparison.Ordinal);
Assert.Contains("Download COMTRADE", selectionWindow, StringComparison.Ordinal);
Assert.Contains("Browse Offline", selectionWindow, StringComparison.Ordinal);