diff --git a/.github/workflows/apply-scl-workflow-layout-repair.yml b/.github/workflows/apply-scl-workflow-layout-repair.yml
new file mode 100644
index 00000000..639491bc
--- /dev/null
+++ b/.github/workflows/apply-scl-workflow-layout-repair.yml
@@ -0,0 +1,110 @@
+name: Apply repaired SCL workflow and layout
+
+on:
+ pull_request:
+ branches: [ main ]
+
+permissions:
+ contents: write
+
+jobs:
+ apply:
+ if: ${{ github.event.pull_request.head.ref == 'fix/scl-workflow-layout-repair' }}
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repair branch
+ uses: actions/checkout@v4
+ with:
+ ref: fix/scl-workflow-layout-repair
+ fetch-depth: 0
+
+ - name: Apply and validate reviewed transformations
+ id: transform
+ continue-on-error: true
+ shell: bash
+ run: |
+ set -u
+ rm -f .repair/apply-error.txt
+ set +e
+ {
+ set -x
+ python3 -m py_compile .repair/apply.py
+ python3 .repair/apply.py
+ base64 --decode .repair/app-icon.b64 > Assets/app-icon.ico
+ grep -q 'Content="{Binding ControlPendingConfirmationLabel}"' MainWindow.xaml
+ ! grep -q 'Content="Confirm Open"' MainWindow.xaml
+ ! grep -q 'Content="Confirm Close"' MainWindow.xaml
+ grep -q 'TryClaimControlConfirmation' MainWindow.xaml.cs
+ grep -q 'SclFindingAggregator.Group(actionableFindings)' MainWindow.xaml.cs
+ grep -q '1.6.9' ArIED61850Tester.csproj
+ test -s Assets/app-icon.ico
+ } > .repair/apply-error.txt 2>&1
+ status=$?
+ set -e
+ exit "$status"
+
+ - name: Upload transformation failure evidence
+ if: steps.transform.outcome == 'failure'
+ uses: actions/upload-artifact@v4
+ with:
+ name: scl-workflow-repair-error
+ path: .repair/apply-error.txt
+ if-no-files-found: error
+
+ - name: Stop after transformation failure
+ if: steps.transform.outcome == 'failure'
+ shell: bash
+ run: exit 1
+
+ - name: Publish repaired non-workflow files
+ if: steps.transform.outcome == 'success'
+ env:
+ GH_TOKEN: ${{ github.token }}
+ TARGET_BRANCH: fix/scl-workflow-layout-final
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ update_existing() {
+ local path="$1"
+ local current_sha local_sha content payload
+ current_sha=$(gh api "repos/$GITHUB_REPOSITORY/contents/$path?ref=$TARGET_BRANCH" --jq .sha)
+ local_sha=$(git hash-object "$path")
+ if [ "$current_sha" = "$local_sha" ]; then
+ echo "$path already current"
+ return
+ fi
+ content=$(base64 -w0 "$path")
+ payload=$(jq -n \
+ --arg message "fix: publish repaired $path" \
+ --arg content "$content" \
+ --arg sha "$current_sha" \
+ --arg branch "$TARGET_BRANCH" \
+ '{message:$message,content:$content,sha:$sha,branch:$branch}')
+ gh api --method PUT "repos/$GITHUB_REPOSITORY/contents/$path" --input - <<<"$payload" >/dev/null
+ }
+
+ create_or_update() {
+ local path="$1"
+ local current_sha local_sha content payload
+ local_sha=$(git hash-object "$path")
+ current_sha=$(gh api "repos/$GITHUB_REPOSITORY/contents/$path?ref=$TARGET_BRANCH" --jq .sha 2>/dev/null || true)
+ if [ -n "$current_sha" ] && [ "$current_sha" = "$local_sha" ]; then
+ echo "$path already current"
+ return
+ fi
+ content=$(base64 -w0 "$path")
+ if [ -n "$current_sha" ]; then
+ payload=$(jq -n --arg message "fix: publish repaired $path" --arg content "$content" --arg sha "$current_sha" --arg branch "$TARGET_BRANCH" '{message:$message,content:$content,sha:$sha,branch:$branch}')
+ else
+ payload=$(jq -n --arg message "docs: add Smart Dynamic SCL workflow" --arg content "$content" --arg branch "$TARGET_BRANCH" '{message:$message,content:$content,branch:$branch}')
+ fi
+ gh api --method PUT "repos/$GITHUB_REPOSITORY/contents/$path" --input - <<<"$payload" >/dev/null
+ }
+
+ update_existing MainWindow.xaml
+ update_existing MainWindow.xaml.cs
+ update_existing ArIED61850Tester.csproj
+ update_existing scripts/publish-windows-portable.ps1
+ update_existing Assets/app-icon.ico
+ create_or_update docs/SCL_SMART_DYNAMIC_REPORTING.md
diff --git a/.repair/app-icon.b64 b/.repair/app-icon.b64
new file mode 100644
index 00000000..fb5d268c
--- /dev/null
+++ b/.repair/app-icon.b64
@@ -0,0 +1 @@
+AAABAAMAMDAAAAEAIACoJQAANgAAACAgAAABACAAqBAAAN4lAAAQEAAAAQAgAGgEAACGNgAAKAAAADAAAABgAAAAAQAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wD///8A////AP///wD///8A////AP///wD///8A////AIgAAA+OBgAtjQMAXIwEAn6MBACJigQCiYkCAoqHAgKKgwICioECAoqAAAaKgAAHin4AC4p+AA+KegARinoAFop6AByKeAAfingCJYp4ACqKegAuinwANIp8AjeKfgA7in8APYl/Aj+JgABAfIEARluCAEkxeQBDEwAAAAH///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wCXAAAWkwcAbZMJAMZ+BwH2bQQB/WQDAf1gAwH9XgIC/VwCAv5bAQL+WQED/VgBA/5XAAT+VwEF/VQBB/5TAAj+UgEL/lIBDf1RARD+UgES/VIBFv1SARn9VAEc/VUBHv1XASH9WAEk/VkBJv1dASn9XgEs/WUBNPx0AUL4hQFP0YYCUYGHAFAgAAAAAf///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AqgAAA6UMAEGeDADMcwcB+1ECAf1HAAH9RAAB/UEAAf0/AAH9PQAB/T0AAf48AAH+OwEA/TsAAP47AAD+OgEA/jkBAP44AAH+NwAC/jcABP43AQb+NwEI/TgBCv05AQz9OgEO/jwBD/0+ARL9PgEU/UEBFf1DARf9RQAY/UcAGfxJARz9TwEh/WcBOPyGAVXeiwBZYYAAQAj///8A////AP///wD///8A////AP///wD///8A////AP///wC/AAAEtRIAYZYPAO5aBAH9RwAB/UIAAf0+AAH9OwAB/TkAAf04AAH9NwAB/TcBAf42AQH+NgEA/jcBAP43AAD+NgEA/jYAAf41AAL+NAAD/jQABf40AQj+NQAJ/TUBC/03AA39OAEP/ToAEf08ARP9PQEV/T4BFv1AARn+QgAZ/UUAGv1HARz9SgEd/U0BIP1VASf9egFN9o4CXoqAAE0K////AP///wD///8A////AP///wD///8A////AP///wDFGwBUnRQA8VEDAP1EAAH9PgAB/Ts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[... ELLIPSIZATION ...]//8A7t4ChOiZAPbfggD92ncA/dNtAP3KYQD9v1UA/LhNAP2wRQD9qDwA/aA1AP2ZLwD9lSsA/ZIoAP6PJQD+kCUA/ZkqAP2uOgL945M8/fzwQv375jj++95I/vvTY/77xob++720/e+Gyv24DXT9lQJU/YgBSv2DAUr9ggFM/YIBT/2FAlP9iAJY/YwCX/2QAmj9lgFx/ZwCev2iAoX9qAKP/a8CnP21Aqj9vwK3+twN47X///8A////AP///wD///8A7OwFNvC+AO/mkAD934IA/dp3AP3TbAD9yV8A/MJWAP25TQD9sEUA/ak8AP2iNAD9nC8A/ZcrAP6VKAD9lCcA/ZcoAv6iLQX9wkoO/fPMbf3763X+++WA/vvek/781Kz++cXO/dIoiP6kB1n9kgFM/okBS/2HAk39iAFQ/YgCVf2MAlv9jgJh/ZMCaf2XAnL9ngJ9/aQChv2qApL9rwKc/bYCqf2+Arb9zAfM8uIS6Ff///8A////AP///wD///8A29sAB/HeAMLtowD75o4A/eGCAP3bdwD90moA/MtgAP3DVgD9uU0A/bFEAP2pOwD9ozQA/Z4vAP6aKwD+mSkB/pknBP6eKAn9qy0Q/shJJP7cXTn+31xE/t9WTv7gT17+1TZn/bAQVf6aBk39kQJM/Y0BTf2NAlL9jgJX/Y8CXf2TAmT+lgJq/ZsCdP2fAn79pgGJ/asCk/2yAqD9twKr/cACuP3IAsj84xLo3tML3hf///8A////AP///wD///8A////AO3oADj0zADt7J8B/eePAP3iggD92XQA/dNqAP3MXwD9wVUA/bhLAP2xQgD9qjoA/aU0AP6hLwD+nisC/Z0oBv6dJgr9oyUR/aolGf6wJSH+syQo/rIfL/6wGjj+qhNB/Z8LR/2YBUn+kwJO/ZICU/2TAlj9lAFf/ZYCZv2aAm7+nQJ1/aICf/2nAov9rgKV/bQCof26Aq/9wQK6/ckCyv3ZDODz5BbsXv///wD///8A////AP///wD///8A////AP//AALv4wBu9MQB9e6fAP3ojgD94IAA/dp0AP3TaQD9yl0A/cFSAP25SgD9skIA/a06AP6oNAD+pC8C/aIrB/6iJwz9oyMS/aUgGv6mHSD+phsm/qQXLv2jEzX9oQ4+/ZwJRv2aBE39mAJT/ZgBWv2aAmL9nAJp/Z4Ccv2jAnz9pQKC/aoCjf2wApr9tgKl/bwCsf3DAsD9ygLO/dYJ3/nnGO+dxgDjCf///wD///8A////AP///wD///8A////AP///wDf3wAI8OQAevTIAPXuoQD95o0A/eGAAP3bdQD90mgA/cpcAP3DUwD9u0sA/bZCAP6xOwD+rDQD/qgvCf6nKg79pyUU/qcgHP6mHCP+pRkq/qMVMv6jETr9og1C/aAISv6eBFL9ngJb/Z8CZf2iAm79pQJ3/acCgf2tAoz9rwGT/bQCn/25Aqv9wAG3/cUBxP3LAtP92w/k+Ocf76LjHOMS////AP///wD///8A////AP///wD///8A////AP///wD///8A//8ABe3kAFT02ADW7q8A/OiRAP3iggD92nQA/dNpAP3MXgD9xVQA/b5KAP65QgD+tDsE/bE0Cv2uLg/9rSkW/a0jH/6sHif+qhsv/qgWN/2pEj/+qA5J/aYKU/2mBlz9pwJn/akCcv2sAn79rwKJ/bIClf24AqH9ugGo/cABtP3FAcL9zAHP/dUJ3vzhG+vm5SPtdNgU6w3///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wDs4wAb8OAAdfLPAMXusQDy6JoA/eOLAP3efgD92HQA/dRqAP3QYgD9zVkD/cpSCP3ISw39xkMW/cY6Iv3FNSz9wi81/cEpP/3CJUr9wSBW/cEbYv3BF279whJ9/cMPjf3GDZz9yAys/coLufzNC8T90AvM/NcO2fzdFOL15R/qzugm7obqIuolqgCqA////wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AAAAAAe3bAA7x1QAk78YAP+y3AFLtqwBV6qIAVeqWAFXqkgBU54kAVOeDA1TneQZU5HAMVORnGFTkYSdU4Vg0VOFSQFTkTExU5EZbVOFAalThOnlU4TGSVOQup1TkJLlU5CHGVeQe1VXkHt5V4x/jUuQi6EPsJuwo4R7hEYAAgAL///8A////AP///wD///8A////AP///wD//wAD8OAAIfDEAFbrrwCC5psAl+KLAJjdfACY2HACmNVlB5jTWQ+Y0ksimNBBMpjOO0OY0DJUmM4qaJjOIoGY0hucmNUWtZjXFMiY2BTTl94X4IXhHOZc5SHrJ8wz/wX///8A////AP///wD///8A8AAAD+AAAAfAAAADgAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAHAAAAD4AAAA/AAAA8oAAAAEAAAACAAAAABACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAYAAAAx4BgBVZQMAtlwBANpYAQHeVQAC3lIAB95QAA7dUgAY3VUAIN1ZACfaYQAxuG8APVx2AE4NgAAAAoAAAAiPEgBwTQIA7ToAAfo2AAD8NAAA/DUAAPwzAAP9NQAJ/DkAEfw9ARj8QgEd/EYBIPpVASnweABKf3EAOQmoJAAjZgwA8TwBAfxHCQL9TgwE/U0JCP1JBhD9NAEJ/TcBE/1TBTv9XAZP/WAHWPxhB1b9UQI2/WIBN/WCAFstpDEAVEcDAfpEBgH8x10S/d9cEf3dSy392Dpb/WEOMP1eCkP91SDR/dsc4v3eJOj82D/k/XULfvxWAjf6gQBWX6M5A15RCAD6mUQO/Pd/Bf32bQr98mIl/fdWQ/3INWT9wiSS/fUZ3P3vHfD98h73/PQj+P3DOdL8YgRQ+4MAX2moQQNeWwwA+qVRFfz4iwL95GwQ/YQmDv3abDP99VlT/fQ4i/3mK8P9lxSX/cgh1fz0Hvj9zzva/WYFVfuGAGRprEgAX2EQAPtsGAP84owX/fWBBf2OMQ39kDsZ/fSBOP35VWf9wzeI/YwQdv3rIOv88DD1/ZQXmv1bAkD7hwBlaLRTAF9sFQH7XA4A/JxGC/3ylAj9124K/mYVBv3LdCr9+X9K/fNQf/3xJr799R/u/cQn0P1oAlX9WAE2+4kAZ2i8XgNfeh0A+2ERAP1zHwT92YoQ/fWSBP6KNAn9fywO/dR8P/3rWXz99ynC/eYs4/2IDH/9XQE8/VwBNvyRAm5ox24DX4wqAPtuFwD8aRUB/aJMD/32rwr+4YIN/VgOBf1nDw394kl5/fY1zP26KLb9bANO/WABOP1pAj/7mAJ9aM+BA1+iOwD7gSIA/XEYAP17HwH91YwX/viqA/2lUxL9njwr/fdXif3kRsv9hAVp/WcBP/1sAUH9egJP+6QCjmjZlgNfuE8A+5kzAP2DIgD9fB4B/aFGCf3yuQr976UP/u+FRP32ap79rB2K/XMDSP1xAUT9fQFS/Y0BafuxAqJo5LIAVtBrAPq1SwD8nDQA/YsmAP2OKQH92Zoc/fnIIP76qWD932ek/YsIV/15AUn9ggFW/ZIBbf2kAYr7xQW9YOvEACfllwDzz2kA/LdMAP2hNQD9lioB/a9EDv3rqU797ZmB/bIhbP2KAlD9iwFb/ZcBcv2pApD9wAO29tUK1THfnwAI7L4AeuOPAO/QaAD6uUkA+6g2AfylKg37rCQj/KsZO/udB0/7mgJj+6MCe/uvAZj6wgS78dYL2orGAMYJ//8AAf+/AAzuuQBY5ZoAsNZ2AMnKWgHKwUUOyr0yKMq7I0TKuhVmyr8NkMrGCrTJzw3Ns9kQ3l7dEd0PgACAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
\ No newline at end of file
diff --git a/.repair/apply.py b/.repair/apply.py
new file mode 100644
index 00000000..f56ee4e3
--- /dev/null
+++ b/.repair/apply.py
@@ -0,0 +1,128 @@
+from pathlib import Path
+import re
+
+
+def read(path: str) -> str:
+ return Path(path).read_text(encoding="utf-8")
+
+
+def write(path: str, text: str) -> None:
+ Path(path).write_text(text, encoding="utf-8", newline="\n")
+
+
+def sub_once(text: str, pattern: str, replacement: str, label: str, flags: int = 0) -> str:
+ if replacement in text:
+ return text
+ updated, count = re.subn(pattern, replacement, text, count=1, flags=flags)
+ if count != 1:
+ raise SystemExit(f"{label}: expected one match, found {count}")
+ return updated
+
+
+xaml_path = "MainWindow.xaml"
+xaml = read(xaml_path)
+
+xaml = sub_once(
+ xaml,
+ r''' \s*\s*\s*\s*\s*''',
+ ''' \n \n \n \n ''',
+ "main grid rows",
+ re.S)
+
+if "Compact navigation row: the large top application bar is removed" not in xaml:
+ pattern = (
+ r'''\n \n'''
+ r''' .*?\n \n\n'''
+ r''' \n'''
+ r''' \n \s*'''
+ r'''\s*'''
+ r'''\s*'''
+ r'''\s*'''
+ r'''\s*''')
+ replacement = ''' \n \n \n \n \n \n \n \n \n \n \n \n \n '''
+ xaml = sub_once(xaml, pattern, replacement, "IED Explorer actions", re.S)
+
+if xaml.count('Content="{Binding ControlPendingConfirmationLabel}"') != 1:
+ raise SystemExit("P0 guard: stable single Confirm button is missing")
+if 'Content="Confirm Open"' in xaml or 'Content="Confirm Close"' in xaml:
+ raise SystemExit("P0 guard: legacy dual Confirm buttons detected")
+write(xaml_path, xaml)
+
+
+cs_path = "MainWindow.xaml.cs"
+cs = read(cs_path)
+
+if "_autoExpandedCommandDevices" not in cs:
+ cs = sub_once(cs, r'''( private bool _connectAllInProgress;\n)''', r'''\1 private readonly HashSet _autoExpandedCommandDevices = new(StringComparer.OrdinalIgnoreCase);\n''', "command auto-expand state")
+
+if "TryAutoExpandCommandPanelOnce(_selectedDevice);" not in cs:
+ cs = sub_once(cs, r'''( RaiseWorkspaceCounts\(\);\n)( // ctlModel inspection)''', r'''\1 TryAutoExpandCommandPanelOnce(_selectedDevice);\n\2''', "selected-device expansion hook")
+
+if "SCL model ready. Choose signals" not in cs:
+ cs = sub_once(cs, r'''( AddLog\("INFO", "SCL", status\);\n)''', r'''\1\n if (firstImported != null && firstImported.Signals.Count > 0)\n {\n AddLog("INFO", "SCL", $"{firstImported.Name}: SCL model ready. Choose signals; saving the selection will continue to endpoint binding, connection, and monitoring.");\n await OpenSignalSelectionWizardAsync(firstImported);\n }\n''', "Open SCL selection workflow")
+
+if "var allowDynamicReporting = ShouldAllowDynamicReportingForScl(signals);" not in cs:
+ cs = sub_once(cs, r''' device\.AllowDynamicDataSetWrites = false;\n''', ''' var allowDynamicReporting = ShouldAllowDynamicReportingForScl(signals);\n device.AllowDynamicDataSetWrites = allowDynamicReporting;\n''', "dynamic reporting policy")
+
+if "Smart Dynamic reporting prepared" not in cs:
+ pattern = (
+ r''' device\.Status = workspace\.RequiresEndpointBinding \? "SCL model ready — bind endpoint" : "SCL model ready";\n'''
+ r''' device\.Detail = workspace\.RequiresEndpointBinding\n'''
+ r''' \? "LD/LN/DO/DA are available offline\..*?"\n'''
+ r''' : "LD/LN/DO/DA were loaded offline\..*?";\n'''
+ r''' device\.AcquisitionMode = "SCL offline design model";''')
+ replacement = ''' device.Status = workspace.RequiresEndpointBinding ? "SCL model ready — bind endpoint" : "SCL model ready";\n device.Detail = allowDynamicReporting\n ? (workspace.RequiresEndpointBinding\n ? "LD/LN/DO/DA are available offline. Static report coverage is incomplete; after signal selection and endpoint binding, ArIED will create an association-scoped dynamic DataSet and use a safe free RCB before polling fallback."\n : "LD/LN/DO/DA were loaded offline. Static report coverage is incomplete; ArIED will use static coverage where available and create an association-scoped dynamic DataSet for uncovered selected signals before polling fallback.")\n : (workspace.RequiresEndpointBinding\n ? "LD/LN/DO/DA are available offline. Press Play to bind an MMS endpoint; no discovery traffic was sent while opening the file."\n : "LD/LN/DO/DA were loaded offline. Play performs a fast MMS association; Re-scan performs full design-versus-live verification.");\n device.AcquisitionMode = allowDynamicReporting\n ? "SCL design • Smart Dynamic reporting prepared"\n : "SCL offline design model";'''
+ cs = sub_once(cs, pattern, replacement, "SCL detail policy", re.S)
+
+if "IsSmartDynamicCapabilityHint" not in cs:
+ pattern = r''' private void LogSclFindings\(string sourceName, IReadOnlyList findings\)\n \{.*?\n \}\n\n private bool EnsureSclEndpointBinding'''
+ replacement = ''' private void LogSclFindings(string sourceName, IReadOnlyList findings)\n {\n var actionableFindings = findings\n .Where(finding => !IsSmartDynamicCapabilityHint(finding))\n .ToArray();\n if (actionableFindings.Length == 0)\n return;\n\n var groups = SclFindingAggregator.Group(actionableFindings);\n if (groups.Count != actionableFindings.Length)\n {\n AddLog(\n "INFO",\n "SCL",\n $"{sourceName} • grouped {actionableFindings.Length} actionable finding(s) into {groups.Count} diagnostic group(s). Full typed evidence remains attached to the SCL workspace.");\n }\n\n foreach (var group in groups.Take(40))\n {\n AddLog(\n SclFindingAggregator.ToLogLevel(group.Severity),\n "SCL",\n $"{sourceName} • {group.Code} [{group.Scope}]: {group.ToDiagnosticMessage()}");\n }\n\n if (groups.Count > 40)\n {\n var omittedRawCount = groups.Skip(40).Sum(group => group.Count);\n AddLog(\n "WARN",\n "SCL",\n $"{groups.Count - 40} additional diagnostic group(s), representing {omittedRawCount} actionable finding(s), were omitted from the live log.");\n }\n\n if (groups.Any(group => SclFindingAggregator.IsBlockingSeverity(group.Severity)))\n MarkDiagnosticAlert();\n }\n\n private static bool IsSmartDynamicCapabilityHint(SclWorkspaceFinding finding)\n {\n if (!finding.Severity.Equals("Warning", StringComparison.OrdinalIgnoreCase))\n return false;\n\n return finding.Code.Equals("SCL_REPORT_DATASET_UNASSIGNED", StringComparison.OrdinalIgnoreCase) ||\n finding.Code.Equals("SCL_REPORT_DATASET_UNRESOLVED", StringComparison.OrdinalIgnoreCase);\n }\n\n private static bool ShouldAllowDynamicReportingForScl(IReadOnlyCollection signals)\n {\n return signals.Any(signal =>\n signal.CanPublishAsSignal &&\n (string.IsNullOrWhiteSpace(signal.DataSetReference) ||\n string.IsNullOrWhiteSpace(signal.ReportControlReference)));\n }\n\n private void TryAutoExpandCommandPanelOnce(Iec61850MonitorDevice? device)\n {\n if (device == null || CommandPanelExpander == null || device.CommandSignals.Count == 0)\n return;\n if (!_autoExpandedCommandDevices.Add(device.DeviceId))\n return;\n\n Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() =>\n {\n if (ReferenceEquals(SelectedDevice, device) && CommandPanelExpander != null)\n CommandPanelExpander.IsExpanded = true;\n }));\n }\n\n private bool EnsureSclEndpointBinding'''
+ cs = sub_once(cs, pattern, replacement, "SCL diagnostic policy", re.S)
+
+if "Saving the selected signals will connect and arm Smart Dynamic reporting" not in cs:
+ cs = sub_once(cs, r''' device\.Detail = "Endpoint bound locally\. Play will fast-connect from the SCL design model; Re-scan performs full comparison\.";''', ''' device.Detail = device.AllowDynamicDataSetWrites\n ? "Endpoint bound locally. Saving the selected signals will connect and arm Smart Dynamic reporting with a safe free RCB before polling fallback."\n : "Endpoint bound locally. Play will fast-connect from the SCL design model; Re-scan performs full comparison.";''', "endpoint detail")
+
+if "TryAutoExpandCommandPanelOnce(device);" not in cs:
+ cs = sub_once(cs, r'''( device\.RefreshCommandSignalProjection\(\);\n RebuildControlFeedbackIndex\(device\);\n)''', r'''\1 TryAutoExpandCommandPanelOnce(device);\n''', "command-ready expansion")
+
+if "? ShouldAllowDynamicReportingForScl(cachedSignals)" not in cs:
+ cs = sub_once(cs, r''' AllowDynamicDataSetWrites = hasSclProvenance \? false : profile\.AllowDynamicDataSetWrites,''', ''' AllowDynamicDataSetWrites = hasSclProvenance\n ? ShouldAllowDynamicReportingForScl(cachedSignals)\n : profile.AllowDynamicDataSetWrites,''', "project restore dynamic policy")
+
+for token in ("TryClaimControlConfirmation", "ControlInspectionBusy", "ControlCommandBusy", "ExecuteClaimedControlAsync"):
+ if token not in cs:
+ raise SystemExit(f"P0 guard: missing {token}")
+if "SclFindingAggregator.Group(actionableFindings)" not in cs:
+ raise SystemExit("P1 guard: grouped diagnostics missing")
+write(cs_path, cs)
+
+
+project_path = "ArIED61850Tester.csproj"
+project = read(project_path)
+for old, new in (("1.6.8.0", "1.6.9.0"), ("1.6.8", "1.6.9")):
+ project = project.replace(old, new)
+write(project_path, project)
+
+workflow_path = ".github/workflows/build.yml"
+workflow = read(workflow_path).replace("-Version 1.6.8", "-Version 1.6.9")
+write(workflow_path, workflow)
+
+publish_path = "scripts/publish-windows-portable.ps1"
+publish = read(publish_path).replace('"1.6.8"', '"1.6.9"').replace("1.6.8 or v1.6.8", "1.6.9 or v1.6.9")
+write(publish_path, publish)
+
+Path("docs/SCL_SMART_DYNAMIC_REPORTING.md").write_text(
+ "# SCL-assisted Smart Dynamic reporting\n\n"
+ "Opening an SCL file remains offline. After the model is loaded, ArIED opens signal selection immediately. Saving a valid live selection continues to endpoint binding, MMS association, and monitoring.\n\n"
+ "Static DataSet and ReportControl coverage is preferred when complete. Missing or partial coverage enables an association-scoped dynamic DataSet for uncovered selected signals and a safe free RCB before polling fallback.\n\n"
+ "Unassigned or unresolved report DataSet warnings remain in the typed SCL workspace but are not promoted into noisy live Diagnostics. Positively resolved empty DataSets and other blocking findings remain actionable.\n\n"
+ "The command panel expands once per IED only after at least one executable command row is ready.\n",
+ encoding="utf-8")
diff --git a/.repair/trigger-final b/.repair/trigger-final
new file mode 100644
index 00000000..08a9b401
--- /dev/null
+++ b/.repair/trigger-final
@@ -0,0 +1 @@
+publish validated repair to fix/scl-workflow-layout-final
diff --git a/ArIED61850Tester.csproj b/ArIED61850Tester.csproj
index c23d31f4..901e0ebd 100644
--- a/ArIED61850Tester.csproj
+++ b/ArIED61850Tester.csproj
@@ -14,9 +14,9 @@
ArIED 61850
ArIED 61850 — IEC 61850 Engineering Workstation
Windows IEC 61850 engineering workstation for SCL-assisted project setup, live MMS model discovery, independent multi-IED monitoring, reporting diagnostics, sequence of events, and guarded Smart Control.
- 1.6.8
- 1.6.8.0
- 1.6.8.0
+ 1.6.9
+ 1.6.9.0
+ 1.6.9.0
iec61850;iec-61850;mms;scl;ied-explorer;multi-ied-monitoring;relay-testing;substation-automation;digital-substation;report-control-block;sequence-of-events;smart-control;wpf;dotnet
GPL-3.0-or-later
$(ARIEC61850_PROJECT)
diff --git a/Assets/app-icon.ico b/Assets/app-icon.ico
index c1d489c0..ffadfc32 100644
Binary files a/Assets/app-icon.ico and b/Assets/app-icon.ico differ
diff --git a/MainWindow.xaml b/MainWindow.xaml
index 4f3b604d..9ba3ceb1 100644
--- a/MainWindow.xaml
+++ b/MainWindow.xaml
@@ -8,45 +8,13 @@
Icon="Assets/app-icon.ico" Closing="Window_Closing">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
@@ -104,7 +72,7 @@
-
@@ -119,9 +87,25 @@
+
+
+
+
-
+
+
+
+
+
@@ -815,7 +799,7 @@
-
+
diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs
index 223d5a95..a0cee355 100644
--- a/MainWindow.xaml.cs
+++ b/MainWindow.xaml.cs
@@ -44,6 +44,7 @@ public partial class MainWindow : Window, INotifyPropertyChanged
private bool _hasUnreadDiagnosticError;
private bool _signalSelectionWizardOpen;
private bool _connectAllInProgress;
+ private readonly HashSet _autoExpandedCommandDevices = new(StringComparer.OrdinalIgnoreCase);
public ObservableCollection Devices { get; } = new();
public BulkObservableCollection GlobalPoints { get; } = new();
@@ -85,6 +86,7 @@ public Iec61850MonitorDevice? SelectedDevice
Raise(nameof(ActiveIedTitle));
Raise(nameof(ActiveIedSubtitle));
RaiseWorkspaceCounts();
+ TryAutoExpandCommandPanelOnce(_selectedDevice);
// ctlModel inspection is preloaded independently of the Expander. Avoid
// changing the row set after the panel's first frame has already painted.
}
@@ -223,6 +225,12 @@ private async void OpenScl_Click(object sender, RoutedEventArgs e)
var status = $"{sourceName}: {document.Ieds.Count} IED/AP workspace(s), {offlineCount} offline model(s), {endpointCount} MMS endpoint(s) — {added} added, {refreshed} refreshed, {retained} active retained.";
SetStatus(status);
AddLog("INFO", "SCL", status);
+
+ if (firstImported != null && firstImported.Signals.Count > 0)
+ {
+ AddLog("INFO", "SCL", $"{firstImported.Name}: SCL model ready. Choose signals; saving the selection will continue to endpoint binding, connection, and monitoring.");
+ await OpenSignalSelectionWizardAsync(firstImported);
+ }
}
catch (OperationCanceledException)
{
@@ -272,7 +280,8 @@ private void ApplySclWorkspaceToDevice(
device.Port = 102;
}
- device.AllowDynamicDataSetWrites = false;
+ var allowDynamicReporting = ShouldAllowDynamicReportingForScl(signals);
+ device.AllowDynamicDataSetWrites = allowDynamicReporting;
device.SclWorkspace = workspace;
device.SclComparison = null;
device.SclSourcePath = document.SourcePath;
@@ -281,10 +290,16 @@ private void ApplySclWorkspaceToDevice(
device.SclAccessPointName = workspace.AccessPointName;
device.HasDiscoveryCache = signals.Count > 0;
device.Status = workspace.RequiresEndpointBinding ? "SCL model ready — bind endpoint" : "SCL model ready";
- device.Detail = workspace.RequiresEndpointBinding
- ? "LD/LN/DO/DA are available offline. Press Play to bind an MMS endpoint; no discovery traffic was sent while opening the file."
- : "LD/LN/DO/DA were loaded offline. Play performs a fast MMS association; Re-scan performs full design-versus-live verification.";
- device.AcquisitionMode = "SCL offline design model";
+ device.Detail = allowDynamicReporting
+ ? (workspace.RequiresEndpointBinding
+ ? "LD/LN/DO/DA are available offline. Static report coverage is incomplete; after signal selection and endpoint binding, ArIED will create an association-scoped dynamic DataSet and use a safe free RCB before polling fallback."
+ : "LD/LN/DO/DA were loaded offline. Static report coverage is incomplete; ArIED will use static coverage where available and create an association-scoped dynamic DataSet for uncovered selected signals before polling fallback.")
+ : (workspace.RequiresEndpointBinding
+ ? "LD/LN/DO/DA are available offline. Press Play to bind an MMS endpoint; no discovery traffic was sent while opening the file."
+ : "LD/LN/DO/DA were loaded offline. Play performs a fast MMS association; Re-scan performs full design-versus-live verification.");
+ device.AcquisitionMode = allowDynamicReporting
+ ? "SCL design • Smart Dynamic reporting prepared"
+ : "SCL offline design model";
foreach (var signal in signals)
{
@@ -306,16 +321,19 @@ private static string BuildSclWorkspaceSummary(SclIedWorkspace workspace)
private void LogSclFindings(string sourceName, IReadOnlyList findings)
{
- if (findings.Count == 0)
+ var actionableFindings = findings
+ .Where(finding => !IsSmartDynamicCapabilityHint(finding))
+ .ToArray();
+ if (actionableFindings.Length == 0)
return;
- var groups = SclFindingAggregator.Group(findings);
- if (groups.Count != findings.Count)
+ var groups = SclFindingAggregator.Group(actionableFindings);
+ if (groups.Count != actionableFindings.Length)
{
AddLog(
"INFO",
"SCL",
- $"{sourceName} • grouped {findings.Count} raw finding(s) into {groups.Count} diagnostic group(s). Full typed evidence remains attached to the SCL workspace.");
+ $"{sourceName} • grouped {actionableFindings.Length} actionable finding(s) into {groups.Count} diagnostic group(s). Full typed evidence remains attached to the SCL workspace.");
}
foreach (var group in groups.Take(40))
@@ -328,19 +346,48 @@ private void LogSclFindings(string sourceName, IReadOnlyList 40)
{
- var omittedRawCount = groups
- .Skip(40)
- .Sum(group => group.Count);
+ var omittedRawCount = groups.Skip(40).Sum(group => group.Count);
AddLog(
"WARN",
"SCL",
- $"{groups.Count - 40} additional diagnostic group(s), representing {omittedRawCount} raw finding(s), were omitted from the live log.");
+ $"{groups.Count - 40} additional diagnostic group(s), representing {omittedRawCount} actionable finding(s), were omitted from the live log.");
}
if (groups.Any(group => SclFindingAggregator.IsBlockingSeverity(group.Severity)))
MarkDiagnosticAlert();
}
+ private static bool IsSmartDynamicCapabilityHint(SclWorkspaceFinding finding)
+ {
+ if (!finding.Severity.Equals("Warning", StringComparison.OrdinalIgnoreCase))
+ return false;
+
+ return finding.Code.Equals("SCL_REPORT_DATASET_UNASSIGNED", StringComparison.OrdinalIgnoreCase) ||
+ finding.Code.Equals("SCL_REPORT_DATASET_UNRESOLVED", StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static bool ShouldAllowDynamicReportingForScl(IReadOnlyCollection signals)
+ {
+ return signals.Any(signal =>
+ signal.CanPublishAsSignal &&
+ (string.IsNullOrWhiteSpace(signal.DataSetReference) ||
+ string.IsNullOrWhiteSpace(signal.ReportControlReference)));
+ }
+
+ private void TryAutoExpandCommandPanelOnce(Iec61850MonitorDevice? device)
+ {
+ if (device == null || CommandPanelExpander == null || device.CommandSignals.Count == 0)
+ return;
+ if (!_autoExpandedCommandDevices.Add(device.DeviceId))
+ return;
+
+ Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() =>
+ {
+ if (ReferenceEquals(SelectedDevice, device) && CommandPanelExpander != null)
+ CommandPanelExpander.IsExpanded = true;
+ }));
+ }
+
private bool EnsureSclEndpointBinding(Iec61850MonitorDevice device)
{
if (!device.RequiresEndpointBinding)
@@ -357,7 +404,9 @@ private bool EnsureSclEndpointBinding(Iec61850MonitorDevice device)
device.IpAddress = wizard.RelayIpAddress;
device.Port = wizard.MmsPort;
device.Status = "SCL model ready";
- device.Detail = "Endpoint bound locally. Play will fast-connect from the SCL design model; Re-scan performs full comparison.";
+ device.Detail = device.AllowDynamicDataSetWrites
+ ? "Endpoint bound locally. Saving the selected signals will connect and arm Smart Dynamic reporting with a safe free RCB before polling fallback."
+ : "Endpoint bound locally. Play will fast-connect from the SCL design model; Re-scan performs full comparison.";
device.RefreshComputed();
NewDeviceIp = device.IpAddress;
NewDevicePort = device.Port.ToString(CultureInfo.InvariantCulture);
@@ -1024,6 +1073,7 @@ await Task.WhenAll(candidates.Select(async signal =>
device.RefreshCommandSignalProjection();
RebuildControlFeedbackIndex(device);
+ TryAutoExpandCommandPanelOnce(device);
SetStatus($"{device.Name}: refreshed {candidates.Length} control value(s).");
}
@@ -1677,7 +1727,9 @@ private async void OpenProject_Click(object sender, RoutedEventArgs e)
LogicalDeviceSummary = profile.LogicalDeviceSummary,
IpAddress = profile.IpAddress,
Port = profile.Port <= 0 ? 102 : profile.Port,
- AllowDynamicDataSetWrites = hasSclProvenance ? false : profile.AllowDynamicDataSetWrites,
+ AllowDynamicDataSetWrites = hasSclProvenance
+ ? ShouldAllowDynamicReportingForScl(cachedSignals)
+ : profile.AllowDynamicDataSetWrites,
SclWorkspace = restoredSclWorkspace,
SclSourcePath = profile.SclSourcePath,
SclSourceSha256 = profile.SclSourceSha256,
diff --git a/docs/SCL_SMART_DYNAMIC_REPORTING.md b/docs/SCL_SMART_DYNAMIC_REPORTING.md
new file mode 100644
index 00000000..96e8ddc9
--- /dev/null
+++ b/docs/SCL_SMART_DYNAMIC_REPORTING.md
@@ -0,0 +1,9 @@
+# SCL-assisted Smart Dynamic reporting
+
+Opening an SCL file remains offline. After the model is loaded, ArIED opens signal selection immediately. Saving a valid live selection continues to endpoint binding, MMS association, and monitoring.
+
+Static DataSet and ReportControl coverage is preferred when complete. Missing or partial coverage enables an association-scoped dynamic DataSet for uncovered selected signals and a safe free RCB before polling fallback.
+
+Unassigned or unresolved report DataSet warnings remain in the typed SCL workspace but are not promoted into noisy live Diagnostics. Positively resolved empty DataSets and other blocking findings remain actionable.
+
+The command panel expands once per IED only after at least one executable command row is ready.
diff --git a/scripts/publish-windows-portable.ps1 b/scripts/publish-windows-portable.ps1
index 3e24c65d..b5ec22ff 100644
--- a/scripts/publish-windows-portable.ps1
+++ b/scripts/publish-windows-portable.ps1
@@ -1,5 +1,5 @@
param(
- [string]$Version = "1.6.8",
+ [string]$Version = "1.6.9",
[string]$Runtime = "win-x64",
[bool]$SingleFile = $true,
[bool]$SelfContained = $true,
@@ -23,7 +23,7 @@ if ($normalizedVersion.StartsWith("v", [System.StringComparison]::OrdinalIgnoreC
$normalizedVersion = $normalizedVersion.Substring(1)
}
if ($normalizedVersion -notmatch '^\d+\.\d+\.\d+([-.][0-9A-Za-z.-]+)?$') {
- throw "Invalid version '$Version'. Use a value such as 1.6.8 or v1.6.8."
+ throw "Invalid version '$Version'. Use a value such as 1.6.9 or v1.6.9."
}
$outputRoot = Join-Path $root "dist"