From 0381ea74dfc796eb6fb3fd0d5f9b086b4ef17458 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 09:56:22 +0700 Subject: [PATCH 01/29] feat(workbench): provide bounded typed MMS report hierarchy for Qt view --- .../src/BrowserReportMemberProjection.hpp | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/apps/ied_simulator/src/BrowserReportMemberProjection.hpp b/apps/ied_simulator/src/BrowserReportMemberProjection.hpp index 2f03d202d..ec2eab421 100644 --- a/apps/ied_simulator/src/BrowserReportMemberProjection.hpp +++ b/apps/ied_simulator/src/BrowserReportMemberProjection.hpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -146,4 +147,117 @@ inline constexpr int kBrowserReportTextLimit = 1024; } return projection; } + +// A bounded, ready-to-render hierarchy. The native report frame remains the +// source of truth; QML must never parse an MMS structure's preview string. +// Positional MMS children deliberately say "Field N": without a verified SCL +// type they must NOT be misidentified as stVal, q, t, origin, etc. +inline constexpr int kBrowserReportTreeChildLimit = 192; +inline constexpr unsigned kBrowserReportTreeDepthLimit = 4U; +inline constexpr int kBrowserReportTreeScalarTextLimit = 160; + +[[nodiscard]] inline QString browserReportTreeKind(const mms::MmsDataKind kind) { + using K = mms::MmsDataKind; + switch (kind) { + case K::structure: return QStringLiteral("STRUCT"); + case K::array: return QStringLiteral("ARRAY"); + case K::bit_string: return QStringLiteral("BIT STRING"); + case K::utc_time: return QStringLiteral("UTC"); + case K::boolean: return QStringLiteral("BOOL"); + case K::integer: return QStringLiteral("INT"); + case K::unsigned_integer: return QStringLiteral("UINT"); + case K::floating_point: return QStringLiteral("FLOAT"); + case K::visible_string: return QStringLiteral("STRING"); + case K::octet_string: return QStringLiteral("OCTETS"); + default: return QStringLiteral("MMS"); + } +} + +[[nodiscard]] inline QVariantList projectBrowserReportTree( + const std::vector& members) { + QVariantList rows; + const auto visibleCount = std::min( + members.size(), static_cast(kBrowserReportVisibleMemberLimit)); + rows.reserve(static_cast(visibleCount) + kBrowserReportTreeChildLimit); + int remainingChildren = kBrowserReportTreeChildLimit; + + // Each nested child is serialized once, with bounded depth and text; all + // labels are explicitly positional unless canonical identity is present. + const auto appendValue = [&](const auto& visit, const mms::MmsDataValue& value, + const int ordinal, const unsigned depth, + const QString& title, const QString& fullReference) -> void { + const bool isContainer = value.kind() == mms::MmsDataKind::structure || + value.kind() == mms::MmsDataKind::array; + QString displayed; + if (isContainer) { + displayed = QStringLiteral("%1 %2").arg(value.children().size()).arg( + value.kind() == mms::MmsDataKind::array + ? QStringLiteral("item(s)") : QStringLiteral("field(s)")); + } else { + const auto preview = previewMmsValue( + value, kBrowserReportTreeScalarTextLimit); + displayed = QString::fromStdString(preview.text); + if (preview.truncated) displayed += QStringLiteral(" …"); + } + // Only a direct, uniquely identified q/t attribute can carry typed + // Quality/TimeQuality meaning. Unknown positional children are raw MMS. + if (depth == 0U && !isContainer) { + const auto evidence = decodeEngineeringEvidence(fullReference, value); + if (!evidence.isEmpty()) + displayed = evidence.value(QStringLiteral("summary")).toString(); + } + rows.push_back(QVariantMap{ + {QStringLiteral("memberIndex"), ordinal}, + {QStringLiteral("depth"), static_cast(depth)}, + {QStringLiteral("name"), title}, + {QStringLiteral("reference"), fullReference}, + {QStringLiteral("value"), displayed}, + {QStringLiteral("kind"), browserReportTreeKind(value.kind())}, + {QStringLiteral("container"), isContainer}, + {QStringLiteral("received"), true} + }); + if (!isContainer || depth >= kBrowserReportTreeDepthLimit) + return; + int index = 0; + for (const auto& child : value.children()) { + if (remainingChildren <= 0) break; + --remainingChildren; + visit(visit, child, ordinal, depth + 1U, + QStringLiteral("Field %1").arg(++index), fullReference); + } + }; + + for (std::size_t i = 0U; i < visibleCount; ++i) { + const auto& member = members[i]; + const std::string* source = &member.data_reference; + if (source->empty() && member.member) source = &member.member->user_reference; + if (source->empty() && member.member) source = &member.member->mms_reference; + bool shortened = false; + const QString fullReference = boundedReportMetadata( + *source, kBrowserReportTextLimit, shortened); + QString name = fullReference; + // Prefer compact canonical IEC paths while keeping full reference in + // the per-row tooltip and never forging a functional constraint. + const int slash = name.indexOf(QLatin1Char('/')); + if (slash >= 0) name = name.mid(slash + 1); + if (name.isEmpty()) name = QStringLiteral("Member %1").arg(member.data_set_index + 1U); + + if (member.value) { + appendValue(appendValue, *member.value, static_cast(i), + 0U, name, fullReference); + } else { + rows.push_back(QVariantMap{ + {QStringLiteral("memberIndex"), static_cast(i)}, + {QStringLiteral("depth"), 0}, + {QStringLiteral("name"), name}, + {QStringLiteral("reference"), fullReference}, + {QStringLiteral("value"), QStringLiteral("Read failed")}, + {QStringLiteral("kind"), QStringLiteral("MMS")}, + {QStringLiteral("container"), false}, + {QStringLiteral("received"), false} + }); + } + } + return rows; +} } // namespace arstack::iedsim From 10b71c9ed8bfbab007b3f2276f416784207c5b48 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 09:56:59 +0700 Subject: [PATCH 02/29] fix(workbench): fail closed on shortened report member identity during typed decode --- .../src/BrowserReportMemberProjection.hpp | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/apps/ied_simulator/src/BrowserReportMemberProjection.hpp b/apps/ied_simulator/src/BrowserReportMemberProjection.hpp index ec2eab421..5ca102bb9 100644 --- a/apps/ied_simulator/src/BrowserReportMemberProjection.hpp +++ b/apps/ied_simulator/src/BrowserReportMemberProjection.hpp @@ -185,12 +185,14 @@ inline constexpr int kBrowserReportTreeScalarTextLimit = 160; // labels are explicitly positional unless canonical identity is present. const auto appendValue = [&](const auto& visit, const mms::MmsDataValue& value, const int ordinal, const unsigned depth, - const QString& title, const QString& fullReference) -> void { + const QString& title, const QString& fullReference, + const bool fullReferenceShortened) -> void { const bool isContainer = value.kind() == mms::MmsDataKind::structure || value.kind() == mms::MmsDataKind::array; QString displayed; if (isContainer) { - displayed = QStringLiteral("%1 %2").arg(value.children().size()).arg( + displayed = QStringLiteral("%1 %2").arg( + static_cast(value.children().size())).arg( value.kind() == mms::MmsDataKind::array ? QStringLiteral("item(s)") : QStringLiteral("field(s)")); } else { @@ -201,7 +203,7 @@ inline constexpr int kBrowserReportTreeScalarTextLimit = 160; } // Only a direct, uniquely identified q/t attribute can carry typed // Quality/TimeQuality meaning. Unknown positional children are raw MMS. - if (depth == 0U && !isContainer) { + if (depth == 0U && !isContainer && !fullReferenceShortened) { const auto evidence = decodeEngineeringEvidence(fullReference, value); if (!evidence.isEmpty()) displayed = evidence.value(QStringLiteral("summary")).toString(); @@ -223,7 +225,8 @@ inline constexpr int kBrowserReportTreeScalarTextLimit = 160; if (remainingChildren <= 0) break; --remainingChildren; visit(visit, child, ordinal, depth + 1U, - QStringLiteral("Field %1").arg(++index), fullReference); + QStringLiteral("Field %1").arg(++index), fullReference, + fullReferenceShortened); } }; @@ -240,11 +243,12 @@ inline constexpr int kBrowserReportTreeScalarTextLimit = 160; // the per-row tooltip and never forging a functional constraint. const int slash = name.indexOf(QLatin1Char('/')); if (slash >= 0) name = name.mid(slash + 1); - if (name.isEmpty()) name = QStringLiteral("Member %1").arg(member.data_set_index + 1U); + if (name.isEmpty()) name = QStringLiteral("Member %1").arg( + static_cast(member.data_set_index + 1U)); if (member.value) { appendValue(appendValue, *member.value, static_cast(i), - 0U, name, fullReference); + 0U, name, fullReference, shortened); } else { rows.push_back(QVariantMap{ {QStringLiteral("memberIndex"), static_cast(i)}, From 8b80360ae837499fb22a7d0ce080029de9b603b8 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 09:57:13 +0700 Subject: [PATCH 03/29] feat(workbench): publish readable hierarchy for latest verified InformationReport --- apps/ied_simulator/src/MmsReportController.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/ied_simulator/src/MmsReportController.cpp b/apps/ied_simulator/src/MmsReportController.cpp index 810b76336..403ade9ff 100644 --- a/apps/ied_simulator/src/MmsReportController.cpp +++ b/apps/ied_simulator/src/MmsReportController.cpp @@ -533,6 +533,12 @@ ReportUi buildSubscriptionUi( const auto projected = arstack::iedsim::projectBrowserReportMembers( frame.values, typedEvidenceBudget); map.insert(QStringLiteral("values"), projected.values); + // Only the newest retained frame gets a deep, bounded hierarchy. + // Older snapshots keep their existing light string previews. + // No second decoder, protocol store or UI-owned report mutation. + if (ui.reports.isEmpty()) + map.insert(QStringLiteral("valueTree"), + arstack::iedsim::projectBrowserReportTree(frame.values)); map.insert(QStringLiteral("valueEvidence"), projected.evidence); map.insert(QStringLiteral("valueEvidenceTruncated"), projected.evidenceTruncated); map.insert(QStringLiteral("valuesTruncated"), projected.rowsTruncated); From fbe0de5dd7654ce1e4c7f88b386035a6c075262a Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 09:58:16 +0700 Subject: [PATCH 04/29] ux(workbench): show report process values before optional bits and diagnostics --- apps/ied_simulator/qml/BrowserReportPane.qml | 95 ++++++++++++-------- 1 file changed, 56 insertions(+), 39 deletions(-) diff --git a/apps/ied_simulator/qml/BrowserReportPane.qml b/apps/ied_simulator/qml/BrowserReportPane.qml index 136d9dff1..caa956c8c 100644 --- a/apps/ied_simulator/qml/BrowserReportPane.qml +++ b/apps/ied_simulator/qml/BrowserReportPane.qml @@ -20,12 +20,19 @@ Rectangle { readonly property bool hasLastReport: latestReport.reportId !== undefined readonly property var configuredMembers: membersForSelectedRcb() property bool attributesExpanded: true - property bool triggerExpanded: true - property bool optionalExpanded: true - property bool informationExpanded: true + // Readable process values first; secondary bit tables and diagnostics + // remain one click away, rather than consuming the opening viewport. + property bool triggerExpanded: false + property bool optionalExpanded: false + property bool informationExpanded: false property bool dataExpanded: true - property bool eventsExpanded: true + property bool eventsExpanded: false property int selectedReportMemberIndex: -1 + readonly property int selectedRcbRow: reports.selectedRcbIndex + onSelectedRcbRowChanged: { + if (detailScroll.contentItem) detailScroll.contentItem.contentY = 0 + selectedReportMemberIndex = -1 + } // Preserve selection through repeat snapshots of the same report frame; // reset only when the actual ReportID/sequence/entry identity changes. readonly property string latestReportIdentity: !hasLastReport ? "" : [ @@ -190,7 +197,7 @@ Rectangle { readonly property var rawValue: lastReportSection ? root.informationValue(index) : root.attributeValue(index) readonly property int stripe: index - implicitHeight: 25 + implicitHeight: 30 Layout.fillWidth: true color: stripe % 2 ? root.theme.surfaceSoft : root.theme.surface Rectangle { @@ -214,7 +221,7 @@ Rectangle { verticalAlignment: Text.AlignVCenter text: propertyRow.title color: root.theme.textSoft - font.pixelSize: 10 + font.pixelSize: 12 elide: Text.ElideRight ToolTip.visible: titleMouse.containsMouse && truncated ToolTip.text: title @@ -227,7 +234,7 @@ Rectangle { verticalAlignment: Text.AlignVCenter text: root.asText(rawValue) color: root.theme.text - font.pixelSize: 10 + font.pixelSize: 12 elide: Text.ElideMiddle ToolTip.visible: valueMouse.containsMouse && truncated ToolTip.text: text @@ -242,7 +249,7 @@ Rectangle { property bool opened: true signal toggleRequested() Layout.fillWidth: true - implicitHeight: 26 + implicitHeight: 30 color: root.theme.surfaceRaised Rectangle { anchors.bottom: parent.bottom; width: parent.width; height: 1; color: root.theme.line } RowLayout { @@ -252,12 +259,12 @@ Rectangle { spacing: 7 Label { text: opened ? "▾" : "▸" - font.pixelSize: 10 + font.pixelSize: 12 color: root.theme.textSoft } Label { text: heading - font.pixelSize: 10 + font.pixelSize: 12 color: root.theme.text font.weight: Font.DemiBold Layout.fillWidth: true @@ -265,7 +272,7 @@ Rectangle { Label { visible: evidence.length > 0 text: evidence - font.pixelSize: 9 + font.pixelSize: 11 color: root.theme.muted } } @@ -287,7 +294,7 @@ Rectangle { ? root.memberValue(modelData) : "—" readonly property int stripe: index Layout.fillWidth: true - implicitHeight: 25 + implicitHeight: 30 color: received && root.selectedReportMemberIndex === index ? root.theme.accentSoft : stripe % 2 ? root.theme.surfaceSoft : root.theme.surface @@ -300,14 +307,14 @@ Rectangle { text: "DA" Layout.preferredWidth: 25 color: root.theme.accent - font.pixelSize: 9 + font.pixelSize: 11 font.weight: Font.DemiBold } Label { text: dataRow.reference Layout.preferredWidth: Math.max(100, parent.width * 0.55 - 28) color: root.theme.text - font.pixelSize: 10 + font.pixelSize: 12 elide: Text.ElideMiddle ToolTip.visible: dataNameMouse.containsMouse && truncated ToolTip.text: text @@ -319,7 +326,7 @@ Rectangle { leftPadding: 8 text: dataRow.displayValue color: root.theme.text - font.pixelSize: 10 + font.pixelSize: 12 elide: Text.ElideMiddle ToolTip.visible: dataValueMouse.containsMouse && truncated ToolTip.text: text @@ -353,7 +360,7 @@ Rectangle { Rectangle { implicitWidth: 20; implicitHeight: 17; radius: 2 color: theme.accent - Label { anchors.centerIn: parent; text: "R"; color: "#ffffff"; font.pixelSize: 10; font.weight: Font.DemiBold } + Label { anchors.centerIn: parent; text: "R"; color: "#ffffff"; font.pixelSize: 12; font.weight: Font.DemiBold } } Label { Layout.fillWidth: true @@ -372,7 +379,7 @@ Rectangle { : root.rcb.probeOk === true ? "VERIFIED" : "ENGINEERING" color: reports.cleanupRequired ? theme.red : root.selectedIsSubscribed ? theme.green : theme.muted - font.pixelSize: 9 + font.pixelSize: 11 } } } @@ -398,9 +405,9 @@ Rectangle { anchors.leftMargin: 8 anchors.rightMargin: 8 spacing: 8 - Label { text: modelData.buffered ? "BR" : "UR"; color: theme.accent; font.pixelSize: 9; Layout.preferredWidth: 22 } - Label { text: modelData.name || modelData.reference || "RCB"; Layout.fillWidth: true; font.pixelSize: 10; color: theme.text; elide: Text.ElideMiddle } - Label { text: modelData.logicalNode || "LLN0"; font.pixelSize: 9; color: theme.muted } + Label { text: modelData.buffered ? "BR" : "UR"; color: theme.accent; font.pixelSize: 11; Layout.preferredWidth: 22 } + Label { text: modelData.name || modelData.reference || "RCB"; Layout.fillWidth: true; font.pixelSize: 12; color: theme.text; elide: Text.ElideMiddle } + Label { text: modelData.logicalNode || "LLN0"; font.pixelSize: 11; color: theme.muted } } MouseArea { id: reportMouse; anchors.fill: parent; hoverEnabled: true; onClicked: reports.selectRcb(index) } } @@ -428,7 +435,7 @@ Rectangle { bottomPadding: 5 color: theme.muted wrapMode: Text.WordWrap - font.pixelSize: 10 + font.pixelSize: 12 text: "Another RCB is actively subscribed on this MMS association: " + reports.subscriptionRcbReference + ". The selected RCB is inspection-only; Stop active RCB acts on " @@ -468,7 +475,7 @@ Rectangle { topPadding: 6 bottomPadding: 6 color: theme.muted - font.pixelSize: 10 + font.pixelSize: 12 text: "TrgOps " + root.readStatusLabel(root.rcb.triggerOptionsReadStatus) + " · no on/off bit state is inferred." } @@ -486,7 +493,7 @@ Rectangle { checked: root.rcb.triggerOptionNames && root.rcb.triggerOptionNames.indexOf(modelData.name) >= 0 enabled: false // inspected bit, never an MMS Write - font.pixelSize: 10 + font.pixelSize: 12 Layout.fillWidth: true } } @@ -510,7 +517,7 @@ Rectangle { topPadding: 6 bottomPadding: 6 color: theme.muted - font.pixelSize: 10 + font.pixelSize: 12 text: "OptFlds " + root.readStatusLabel(root.rcb.optionalFieldsReadStatus) + " · no on/off bit state is inferred." } @@ -528,7 +535,7 @@ Rectangle { checked: root.rcb.optionalFieldNames && root.rcb.optionalFieldNames.indexOf(modelData.name) >= 0 enabled: false // inspected bit, never an MMS Write - font.pixelSize: 10 + font.pixelSize: 12 Layout.fillWidth: true } } @@ -543,7 +550,7 @@ Rectangle { anchors.fill: parent anchors.margins: 7 wrapMode: Text.WordWrap - font.pixelSize: 10 + font.pixelSize: 12 color: root.observedRcbInUse() ? theme.red : theme.muted text: root.rcb.probeOk !== true ? "RCB live state not verified · never infer an unused reservation." @@ -585,19 +592,26 @@ Rectangle { spacing: 0 Rectangle { Layout.fillWidth: true - implicitHeight: 25 + implicitHeight: 30 color: theme.chrome RowLayout { anchors.fill: parent anchors.leftMargin: 8 anchors.rightMargin: 8 - Label { text: "Name"; Layout.preferredWidth: Math.max(100, parent.width * 0.55 - 1); color: theme.textSoft; font.pixelSize: 10; font.weight: Font.DemiBold } - Label { text: "Value"; Layout.fillWidth: true; color: theme.textSoft; font.pixelSize: 10; font.weight: Font.DemiBold } + Label { text: "Name"; Layout.preferredWidth: Math.max(100, parent.width * 0.55 - 1); color: theme.textSoft; font.pixelSize: 12; font.weight: Font.DemiBold } + Label { text: "Value"; Layout.fillWidth: true; color: theme.textSoft; font.pixelSize: 12; font.weight: Font.DemiBold } } } Repeater { - model: root.hasLastReport && root.latestReport.values - ? root.latestReport.values : [] + model: root.hasLastReport && root.latestReport.valueTree + ? root.latestReport.valueTree : [] + delegate: TreeDataRow {} + } + Repeater { + model: root.hasLastReport && + (!root.latestReport.valueTree || + root.latestReport.valueTree.length === 0) + && root.latestReport.values ? root.latestReport.values : [] delegate: DataRow { received: true } } Repeater { @@ -606,7 +620,10 @@ Rectangle { } BrowserEngineeringEvidence { theme: root.theme - engineeringEvidence: root.selectedReportMemberEvidence() + engineeringEvidence: root.hasLastReport && + (!root.latestReport.valueTree || + root.latestReport.valueTree.length === 0) + ? root.selectedReportMemberEvidence() : ({}) } Label { visible: root.hasLastReport && @@ -615,7 +632,7 @@ Rectangle { leftPadding: 9 text: "Some report q/t details were omitted by the bounded projection." color: theme.muted - font.pixelSize: 9 + font.pixelSize: 11 } Label { visible: root.hasLastReport && @@ -624,7 +641,7 @@ Rectangle { leftPadding: 9 text: "Some values are preview-truncated; the report payload is preserved by MMS runtime." color: theme.muted - font.pixelSize: 9 + font.pixelSize: 11 } Label { visible: root.hasLastReport ? !root.latestReport.values || root.latestReport.values.length === 0 @@ -639,7 +656,7 @@ Rectangle { ? "No bound DataSet reference is available for this RCB." : "The configured DataSet has no exposed members. Offline values are not fabricated." color: theme.muted - font.pixelSize: 10 + font.pixelSize: 12 } Label { visible: root.hasLastReport ? root.latestReport.valuesTruncated === true @@ -648,7 +665,7 @@ Rectangle { leftPadding: 10 text: "Showing first 256 members. Narrow the context for more." color: theme.muted - font.pixelSize: 9 + font.pixelSize: 11 } } @@ -672,7 +689,7 @@ Rectangle { bottomPadding: 3 text: modelData color: theme.textSoft - font.pixelSize: 9 + font.pixelSize: 11 wrapMode: Text.WordWrap } } @@ -690,7 +707,7 @@ Rectangle { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: 8 - font.pixelSize: 10 + font.pixelSize: 12 wrapMode: Text.WordWrap color: theme.amber text: reports.rejectedReportCount + " foreign/stale report(s) withheld from process values · " @@ -708,7 +725,7 @@ Rectangle { id: issueLabel anchors.fill: parent anchors.margins: 8 - font.pixelSize: 10 + font.pixelSize: 12 wrapMode: Text.WordWrap color: theme.red text: reports.cleanupRequired From 5670a3e3767cdd31669456569c194d102a44f274 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 09:58:36 +0700 Subject: [PATCH 05/29] ux(workbench): present bounded expandable MMS report tree instead of dense structure strings --- apps/ied_simulator/qml/BrowserReportPane.qml | 94 ++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/apps/ied_simulator/qml/BrowserReportPane.qml b/apps/ied_simulator/qml/BrowserReportPane.qml index caa956c8c..949ec46c4 100644 --- a/apps/ied_simulator/qml/BrowserReportPane.qml +++ b/apps/ied_simulator/qml/BrowserReportPane.qml @@ -342,6 +342,100 @@ Rectangle { Rectangle { anchors.bottom: parent.bottom; width: parent.width; height: 1; color: root.theme.lineSoft } } + // Presentation-only tree over authoritative MMS-decoded child values. + // Unknown child types retain positional names; SCL labels are never guessed. + component TreeDataRow: ColumnLayout { + id: treeRow + required property int index + required property var modelData + readonly property bool topLevel: modelData.depth === 0 + readonly property bool expanded: root.selectedReportMemberIndex === modelData.memberIndex + visible: topLevel || expanded + Layout.fillWidth: true + spacing: 0 + + Rectangle { + Layout.fillWidth: true + implicitHeight: 32 + color: treeRow.topLevel && treeRow.expanded + ? root.theme.accentSoft + : treeRow.index % 2 ? root.theme.surfaceSoft : root.theme.surface + RowLayout { + anchors.fill: parent + anchors.leftMargin: 9 + Math.min(treeRow.modelData.depth, 4) * 16 + anchors.rightMargin: 8 + spacing: 6 + Label { + Layout.preferredWidth: 21 + text: treeRow.topLevel + ? (treeRow.modelData.container + ? (treeRow.expanded ? "▾" : "▸") : "DA") + : "·" + color: root.theme.accent + font.pixelSize: 12 + font.weight: Font.DemiBold + } + Label { + Layout.preferredWidth: Math.max(100, parent.width * 0.49 - 35) + text: treeRow.modelData.name || "—" + color: root.theme.text + font.pixelSize: 12 + font.weight: treeRow.topLevel ? Font.DemiBold : Font.Normal + elide: Text.ElideMiddle + ToolTip.visible: treeNameHover.containsMouse && truncated + ToolTip.text: treeRow.modelData.reference || text + MouseArea { + id: treeNameHover + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + } + } + Label { + Layout.fillWidth: true + text: treeRow.modelData.value || "—" + color: treeRow.modelData.received ? root.theme.text : root.theme.muted + font.pixelSize: 12 + elide: Text.ElideRight + ToolTip.visible: treeValueHover.containsMouse && truncated + ToolTip.text: text + MouseArea { + id: treeValueHover + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + } + } + Label { + visible: !treeRow.topLevel + Layout.preferredWidth: 66 + text: treeRow.modelData.kind || "" + color: root.theme.muted + font.pixelSize: 11 + horizontalAlignment: Text.AlignRight + } + } + MouseArea { + anchors.fill: parent + enabled: treeRow.topLevel + cursorShape: Qt.PointingHandCursor + onClicked: root.selectedReportMemberIndex = treeRow.expanded + ? -1 : treeRow.modelData.memberIndex + } + Rectangle { + anchors.bottom: parent.bottom + width: parent.width + height: 1 + color: root.theme.lineSoft + } + } + BrowserEngineeringEvidence { + theme: root.theme + engineeringEvidence: treeRow.topLevel && treeRow.expanded + ? root.selectedReportMemberEvidence() : ({}) + } + } + ColumnLayout { anchors.fill: parent spacing: 0 From 643fac3a4238f0d686f1d5087b03dc57ad58bd45 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 09:59:17 +0700 Subject: [PATCH 06/29] fix(workbench): expose nested report preview truncation without hidden loss --- apps/ied_simulator/src/BrowserReportMemberProjection.hpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/ied_simulator/src/BrowserReportMemberProjection.hpp b/apps/ied_simulator/src/BrowserReportMemberProjection.hpp index 5ca102bb9..77fbd55e3 100644 --- a/apps/ied_simulator/src/BrowserReportMemberProjection.hpp +++ b/apps/ied_simulator/src/BrowserReportMemberProjection.hpp @@ -174,7 +174,9 @@ inline constexpr int kBrowserReportTreeScalarTextLimit = 160; } [[nodiscard]] inline QVariantList projectBrowserReportTree( - const std::vector& members) { + const std::vector& members, + bool* nestedFieldsTruncated = nullptr) { + if (nestedFieldsTruncated) *nestedFieldsTruncated = false; QVariantList rows; const auto visibleCount = std::min( members.size(), static_cast(kBrowserReportVisibleMemberLimit)); @@ -222,7 +224,10 @@ inline constexpr int kBrowserReportTreeScalarTextLimit = 160; return; int index = 0; for (const auto& child : value.children()) { - if (remainingChildren <= 0) break; + if (remainingChildren <= 0) { + if (nestedFieldsTruncated) *nestedFieldsTruncated = true; + break; + } --remainingChildren; visit(visit, child, ordinal, depth + 1U, QStringLiteral("Field %1").arg(++index), fullReference, From a6b8b311d8bd9f4500068ca3299f680dd8b64ded Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 09:59:27 +0700 Subject: [PATCH 07/29] fix(workbench): surface only genuine limited nested InformationReport preview --- apps/ied_simulator/src/MmsReportController.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/ied_simulator/src/MmsReportController.cpp b/apps/ied_simulator/src/MmsReportController.cpp index 403ade9ff..b0af52f39 100644 --- a/apps/ied_simulator/src/MmsReportController.cpp +++ b/apps/ied_simulator/src/MmsReportController.cpp @@ -536,9 +536,13 @@ ReportUi buildSubscriptionUi( // Only the newest retained frame gets a deep, bounded hierarchy. // Older snapshots keep their existing light string previews. // No second decoder, protocol store or UI-owned report mutation. - if (ui.reports.isEmpty()) + if (ui.reports.isEmpty()) { + bool nestedTruncated = false; map.insert(QStringLiteral("valueTree"), - arstack::iedsim::projectBrowserReportTree(frame.values)); + arstack::iedsim::projectBrowserReportTree( + frame.values, &nestedTruncated)); + map.insert(QStringLiteral("treeNestedTruncated"), nestedTruncated); + } map.insert(QStringLiteral("valueEvidence"), projected.evidence); map.insert(QStringLiteral("valueEvidenceTruncated"), projected.evidenceTruncated); map.insert(QStringLiteral("valuesTruncated"), projected.rowsTruncated); From ff2aa707dee587fbce4376266bdeab393fa27e81 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 09:59:38 +0700 Subject: [PATCH 08/29] ux(workbench): show clipped nested field evidence only when actually truncated --- apps/ied_simulator/qml/BrowserReportPane.qml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/apps/ied_simulator/qml/BrowserReportPane.qml b/apps/ied_simulator/qml/BrowserReportPane.qml index 949ec46c4..c079126eb 100644 --- a/apps/ied_simulator/qml/BrowserReportPane.qml +++ b/apps/ied_simulator/qml/BrowserReportPane.qml @@ -712,6 +712,17 @@ Rectangle { model: !root.hasLastReport ? root.configuredMembers.slice(0, 256) : [] delegate: DataRow { received: false } } + Label { + visible: root.hasLastReport && + root.latestReport.treeNestedTruncated === true + Layout.fillWidth: true + leftPadding: 9 + topPadding: 3 + bottomPadding: 3 + text: "Nested values exceed the bounded preview. Native MMS frame retained." + color: theme.muted + font.pixelSize: 11 + } BrowserEngineeringEvidence { theme: root.theme engineeringEvidence: root.hasLastReport && From 5507af941b070e622c12680cd067c792ed1113b0 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 10:00:36 +0700 Subject: [PATCH 09/29] test(workbench): native MMS report tree layout, false, q semantics and truncation regressions --- .../src/ied_report_workspace_qa.cpp | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/apps/ied_simulator/src/ied_report_workspace_qa.cpp b/apps/ied_simulator/src/ied_report_workspace_qa.cpp index e65d06704..01e67b26e 100644 --- a/apps/ied_simulator/src/ied_report_workspace_qa.cpp +++ b/apps/ied_simulator/src/ied_report_workspace_qa.cpp @@ -1,5 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later +#include "BrowserReportMemberProjection.hpp" #include "IedFleetController.hpp" #include "IedReportControlManifest.hpp" #include "MmsReportController.hpp" @@ -17,9 +18,77 @@ #include #include +#include #include +#include +#include namespace { +bool verifyReadableReportTreeProjection() { + namespace mms = ar::iec61850::mms; + using arstack::iedsim::projectBrowserReportTree; + // Positional MMS child identity must NEVER be invented as stVal/q/t. + mms::MmsReportValue structured; + structured.data_set_index = 0U; + structured.data_reference = "IED/LLN0.Pos"; + structured.value = mms::MmsDataValue::structure({ + mms::MmsDataValue::boolean(false), + mms::MmsDataValue::integer(42), + mms::MmsDataValue::array({mms::MmsDataValue::boolean(true)}) + }); + const std::array goodQuality{0x00U, 0x00U}; + mms::MmsReportValue quality; + quality.data_set_index = 1U; + quality.data_reference = "IED/LLN0.Pos.q"; + quality.value = mms::MmsDataValue::bit_string(3U, + std::span{goodQuality}); + mms::MmsReportValue notRead; + notRead.data_set_index = 2U; + notRead.data_reference = "IED/LLN0.NotRead"; + + const auto rows = projectBrowserReportTree({structured, quality, notRead}); + if (rows.size() != 7) return false; // struct, 3 children + 1 array item, q, unread + const auto parent = rows.at(0).toMap(); + const auto child = rows.at(1).toMap(); + const auto nested = rows.at(4).toMap(); + const auto q = rows.at(5).toMap(); + const auto unread = rows.at(6).toMap(); + if (parent.value("name").toString() != QStringLiteral("LLN0.Pos") || + parent.value("depth").toInt() != 0 || + parent.value("value").toString() != QStringLiteral("3 field(s)") || + child.value("name").toString() != QStringLiteral("Field 1") || + child.value("value").toString() != QStringLiteral("false") || + child.value("depth").toInt() != 1 || + nested.value("depth").toInt() != 2 || + q.value("name").toString() != QStringLiteral("LLN0.Pos.q") || + q.value("value").toString() != QStringLiteral("good") || + unread.value("received").toBool() || + unread.value("value").toString() != QStringLiteral("Read failed")) + return false; + + // The shared projection is bounded even for hostile deep/long MMS values. + mms::MmsReportValue oversized; + oversized.data_reference = "IED/LLN0.Large"; + oversized.value = mms::MmsDataValue::structure( + std::vector( + 500U, mms::MmsDataValue::boolean(false))); + bool truncated = false; + const auto limited = projectBrowserReportTree({oversized}, &truncated); + if (!truncated || + limited.size() != arstack::iedsim::kBrowserReportTreeChildLimit + 1) + return false; + // Shortened references must not be used to infer q/t semantic identity. + mms::MmsReportValue ambiguous; + ambiguous.data_reference = std::string(1500U, 'X') + ".q"; + ambiguous.value = mms::MmsDataValue::bit_string(3U, + std::span{goodQuality}); + const auto raw = projectBrowserReportTree({ambiguous}); + if (raw.isEmpty() || raw.first().toMap().value("value").toString() + == QStringLiteral("good")) + return false; + return true; +} + bool isIndexedReportFixture(const QString& sclPath) { return QFileInfo(sclPath).fileName().endsWith(QStringLiteral("indexed-reports.scd"), Qt::CaseInsensitive); } @@ -167,6 +236,7 @@ bool reportProjectionPresent(const MmsReportController& reports) { !frame.value(QStringLiteral("rcbReference")).toString().isEmpty() && !frame.value(QStringLiteral("dataSet")).toString().isEmpty() && !frame.value(QStringLiteral("values")).toStringList().isEmpty() && + !frame.value(QStringLiteral("valueTree")).toList().isEmpty() && frame.contains(QStringLiteral("entryId")) && frame.contains(QStringLiteral("overflow")); } @@ -181,6 +251,11 @@ int main(int argc, char* argv[]) { return 2; } + if (!verifyReadableReportTreeProjection()) { + qCritical() << "REPORTS_WORKBENCH_FAIL typed_tree_and_bounds"; + return 38; + } + if (!verifyIndexedRcbManifestExpansion()) { qCritical() << "REPORTS_WORKBENCH_FAIL indexed_rcb_manifest"; return 20; From 477ea3a78e4c6feec8752da74c065e0d6425cfe5 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 10:01:07 +0700 Subject: [PATCH 10/29] test(workbench): enforce native bounded typed tree and usable inspector opening state --- .../ied_simulator/test_browser_convergence.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/apps/ied_simulator/test_browser_convergence.py b/apps/ied_simulator/test_browser_convergence.py index 9b76d3ac6..d5207e418 100644 --- a/apps/ied_simulator/test_browser_convergence.py +++ b/apps/ied_simulator/test_browser_convergence.py @@ -219,6 +219,50 @@ 'QString::fromStdString(reason)' in projection_header): raise SystemExit("BROWSER_REPORT_METADATA_BOUNDED_FAIL eager untrusted metadata conversion") +# A live InformationReport may carry deeply nested MMS structures. The +# inspector must consume bounded native typed rows, never split "{...}" in QML +# or allocate a full hierarchy for every retained historical frame. +for token in ( + 'kBrowserReportTreeChildLimit = 192', + 'kBrowserReportTreeDepthLimit = 4U', + 'kBrowserReportTreeScalarTextLimit = 160', + 'projectBrowserReportTree(', + 'bool* nestedFieldsTruncated = nullptr', + 'QStringLiteral("Field %1")', + 'decodeEngineeringEvidence(fullReference, value)', + 'depth == 0U && !isContainer && !fullReferenceShortened', + 'if (remainingChildren <= 0) {', +): + if token not in projection_header: + raise SystemExit("BROWSER_REPORT_TYPED_TREE_FAIL missing=" + token) +for token in ( + 'if (ui.reports.isEmpty()) {', + 'QStringLiteral("valueTree")', + 'QStringLiteral("treeNestedTruncated")', + 'projectBrowserReportTree(', +): + if token not in report_controller: + raise SystemExit("BROWSER_REPORT_TYPED_TREE_FAIL controller missing=" + token) +for token in ( + 'component TreeDataRow: ColumnLayout {', + 'delegate: TreeDataRow {}', + 'root.latestReport.valueTree', + 'treeRow.modelData.memberIndex', + 'treeRow.modelData.value', + 'treeRow.topLevel || expanded', + 'property bool triggerExpanded: false', + 'property bool optionalExpanded: false', + 'property bool informationExpanded: false', + 'onSelectedRcbRowChanged:', + 'root.latestReport.treeNestedTruncated === true', +): + if token not in report_inspector: + raise SystemExit("BROWSER_REPORT_TYPED_TREE_FAIL QML missing=" + token) +if 'text: root.memberValue(modelData)' in report_inspector: + raise SystemExit("BROWSER_REPORT_TYPED_TREE_FAIL concatenated-string value in active inspector") +if report_controller.count('projectBrowserReportTree(') != 1: + raise SystemExit("BROWSER_REPORT_TYPED_TREE_FAIL per-frame structure expansion") + value_preview = (workspace_path.parent.parent / "src" / "BrowserBoundedMmsPreview.hpp").read_text(encoding="utf-8") for token in [ 'BoundedMmsPreview', From 3245070eed7db822ad0f1b78a39930eb2d2e435b Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 10:01:21 +0700 Subject: [PATCH 11/29] test(workbench): match actual tree-row visibility binding --- apps/ied_simulator/test_browser_convergence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ied_simulator/test_browser_convergence.py b/apps/ied_simulator/test_browser_convergence.py index d5207e418..bf53095e5 100644 --- a/apps/ied_simulator/test_browser_convergence.py +++ b/apps/ied_simulator/test_browser_convergence.py @@ -249,7 +249,7 @@ 'root.latestReport.valueTree', 'treeRow.modelData.memberIndex', 'treeRow.modelData.value', - 'treeRow.topLevel || expanded', + 'visible: topLevel || expanded', 'property bool triggerExpanded: false', 'property bool optionalExpanded: false', 'property bool informationExpanded: false', From b0959b1747ccb6e67124fd206d76b06e2e100fea Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 10:04:00 +0700 Subject: [PATCH 12/29] ux(workbench): replace gray disabled TrgOps/OptFlds controls with legible passive indicators --- apps/ied_simulator/qml/BrowserReportPane.qml | 72 ++++++++++++++++---- 1 file changed, 60 insertions(+), 12 deletions(-) diff --git a/apps/ied_simulator/qml/BrowserReportPane.qml b/apps/ied_simulator/qml/BrowserReportPane.qml index c079126eb..73da0b0c4 100644 --- a/apps/ied_simulator/qml/BrowserReportPane.qml +++ b/apps/ied_simulator/qml/BrowserReportPane.qml @@ -581,14 +581,38 @@ Rectangle { columnSpacing: 8 Repeater { model: root.triggerFields - delegate: CheckBox { + delegate: RowLayout { required property var modelData - text: modelData.label - checked: root.rcb.triggerOptionNames - && root.rcb.triggerOptionNames.indexOf(modelData.name) >= 0 - enabled: false // inspected bit, never an MMS Write - font.pixelSize: 12 + readonly property bool bitSet: root.rcb.triggerOptionNames + && root.rcb.triggerOptionNames.indexOf(modelData.name) >= 0 Layout.fillWidth: true + Layout.minimumHeight: 30 + spacing: 6 + // Passive bit observation: no CheckBox/MouseArea and + // no disabled-text styling or MMS Write affordance. + Rectangle { + Layout.preferredWidth: 16 + Layout.preferredHeight: 16 + radius: 3 + color: bitSet ? theme.accentSoft : theme.surface + border.width: 1 + border.color: bitSet ? theme.accent : theme.line + Label { + anchors.centerIn: parent + text: "✓" + visible: bitSet + color: theme.accent + font.pixelSize: 12 + font.weight: Font.Bold + } + } + Label { + text: modelData.label + color: theme.text + font.pixelSize: 12 + elide: Text.ElideRight + Layout.fillWidth: true + } } } } @@ -623,14 +647,38 @@ Rectangle { columnSpacing: 8 Repeater { model: root.optionalFields - delegate: CheckBox { + delegate: RowLayout { required property var modelData - text: modelData.label - checked: root.rcb.optionalFieldNames - && root.rcb.optionalFieldNames.indexOf(modelData.name) >= 0 - enabled: false // inspected bit, never an MMS Write - font.pixelSize: 12 + readonly property bool bitSet: root.rcb.optionalFieldNames + && root.rcb.optionalFieldNames.indexOf(modelData.name) >= 0 Layout.fillWidth: true + Layout.minimumHeight: 30 + spacing: 6 + // Passive bit observation: no CheckBox/MouseArea and + // no disabled-text styling or MMS Write affordance. + Rectangle { + Layout.preferredWidth: 16 + Layout.preferredHeight: 16 + radius: 3 + color: bitSet ? theme.accentSoft : theme.surface + border.width: 1 + border.color: bitSet ? theme.accent : theme.line + Label { + anchors.centerIn: parent + text: "✓" + visible: bitSet + color: theme.accent + font.pixelSize: 12 + font.weight: Font.Bold + } + } + Label { + text: modelData.label + color: theme.text + font.pixelSize: 12 + elide: Text.ElideRight + Layout.fillWidth: true + } } } } From 349269fc7c819e518ea322858dd0756445f105db Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 10:04:14 +0700 Subject: [PATCH 13/29] fix(workbench): use explicit bit indicator delegate scope for QML bindings --- apps/ied_simulator/qml/BrowserReportPane.qml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/ied_simulator/qml/BrowserReportPane.qml b/apps/ied_simulator/qml/BrowserReportPane.qml index 73da0b0c4..89bc20f04 100644 --- a/apps/ied_simulator/qml/BrowserReportPane.qml +++ b/apps/ied_simulator/qml/BrowserReportPane.qml @@ -582,6 +582,7 @@ Rectangle { Repeater { model: root.triggerFields delegate: RowLayout { + id: bitRow required property var modelData readonly property bool bitSet: root.rcb.triggerOptionNames && root.rcb.triggerOptionNames.indexOf(modelData.name) >= 0 @@ -594,13 +595,13 @@ Rectangle { Layout.preferredWidth: 16 Layout.preferredHeight: 16 radius: 3 - color: bitSet ? theme.accentSoft : theme.surface + color: bitRow.bitSet ? theme.accentSoft : theme.surface border.width: 1 - border.color: bitSet ? theme.accent : theme.line + border.color: bitRow.bitSet ? theme.accent : theme.line Label { anchors.centerIn: parent text: "✓" - visible: bitSet + visible: bitRow.bitSet color: theme.accent font.pixelSize: 12 font.weight: Font.Bold @@ -648,6 +649,7 @@ Rectangle { Repeater { model: root.optionalFields delegate: RowLayout { + id: bitRow required property var modelData readonly property bool bitSet: root.rcb.optionalFieldNames && root.rcb.optionalFieldNames.indexOf(modelData.name) >= 0 @@ -660,13 +662,13 @@ Rectangle { Layout.preferredWidth: 16 Layout.preferredHeight: 16 radius: 3 - color: bitSet ? theme.accentSoft : theme.surface + color: bitRow.bitSet ? theme.accentSoft : theme.surface border.width: 1 - border.color: bitSet ? theme.accent : theme.line + border.color: bitRow.bitSet ? theme.accent : theme.line Label { anchors.centerIn: parent text: "✓" - visible: bitSet + visible: bitRow.bitSet color: theme.accent font.pixelSize: 12 font.weight: Font.Bold From 9880c7deea2ec809ecf59cb777eabeb9e6aa5997 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 10:04:25 +0700 Subject: [PATCH 14/29] test(workbench): require readable passive status bits not disabled widgets --- apps/ied_simulator/test_browser_convergence.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/ied_simulator/test_browser_convergence.py b/apps/ied_simulator/test_browser_convergence.py index bf53095e5..d7569cd9d 100644 --- a/apps/ied_simulator/test_browser_convergence.py +++ b/apps/ied_simulator/test_browser_convergence.py @@ -311,7 +311,9 @@ 'root.rcb.optionalFieldsReadStatus === "verified"', 'root.rcb.triggerOptionNames.indexOf(modelData.name)', 'root.rcb.optionalFieldNames.indexOf(modelData.name)', - 'enabled: false // inspected bit, never an MMS Write', + 'Passive bit observation: no CheckBox/MouseArea', + 'readonly property bool bitSet:', + 'visible: bitRow.bitSet', 'no on/off bit state is inferred.', 'RCB in use: enabled, reservation or Owner evidence is present.', @@ -319,6 +321,10 @@ missing_report = [term for term in report_required if term not in report_inspector] if missing_report: raise SystemExit("BROWSER_REPORT_INSPECTOR_FAIL missing=" + ",".join(missing_report)) +if 'delegate: CheckBox {' in report_inspector: + raise SystemExit("BROWSER_REPORT_LEGIBILITY_FAIL disabled native checkbox leaked") +if report_inspector.count('id: bitRow') != 2: + raise SystemExit("BROWSER_REPORT_LEGIBILITY_FAIL typed read-only bit indicators missing") if 'String(frame.reportId) === wanted' in report_inspector: raise SystemExit("BROWSER_REPORT_INSPECTOR_FAIL report ID alone is not exact RCB provenance") if 'title: modelData[0]' in report_inspector or 'rawValue: modelData[1]' in report_inspector: From 7df7cb880d29495826393aaeacf20d574191377c Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 10:05:11 +0700 Subject: [PATCH 15/29] fix(workbench): preserve typed q/t evidence, member inclusion reasons and depth-limit warnings --- .../src/BrowserReportMemberProjection.hpp | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/apps/ied_simulator/src/BrowserReportMemberProjection.hpp b/apps/ied_simulator/src/BrowserReportMemberProjection.hpp index 77fbd55e3..8e740a848 100644 --- a/apps/ied_simulator/src/BrowserReportMemberProjection.hpp +++ b/apps/ied_simulator/src/BrowserReportMemberProjection.hpp @@ -203,14 +203,16 @@ inline constexpr int kBrowserReportTreeScalarTextLimit = 160; displayed = QString::fromStdString(preview.text); if (preview.truncated) displayed += QStringLiteral(" …"); } - // Only a direct, uniquely identified q/t attribute can carry typed - // Quality/TimeQuality meaning. Unknown positional children are raw MMS. + // Keep the complete shared decoder evidence with the exact direct + // q/t leaf so operator flags remain inspectable on row expansion. + QVariantMap typedEvidence; if (depth == 0U && !isContainer && !fullReferenceShortened) { - const auto evidence = decodeEngineeringEvidence(fullReference, value); - if (!evidence.isEmpty()) - displayed = evidence.value(QStringLiteral("summary")).toString(); + typedEvidence = decodeEngineeringEvidence(fullReference, value); + if (!typedEvidence.isEmpty()) + displayed = typedEvidence.value(QStringLiteral("summary")).toString(); } rows.push_back(QVariantMap{ + {QStringLiteral("typedEvidence"), typedEvidence}, {QStringLiteral("memberIndex"), ordinal}, {QStringLiteral("depth"), static_cast(depth)}, {QStringLiteral("name"), title}, @@ -220,8 +222,12 @@ inline constexpr int kBrowserReportTreeScalarTextLimit = 160; {QStringLiteral("container"), isContainer}, {QStringLiteral("received"), true} }); - if (!isContainer || depth >= kBrowserReportTreeDepthLimit) + if (!isContainer) return; + if (depth >= kBrowserReportTreeDepthLimit) { + if (!value.children().empty() && nestedFieldsTruncated) + *nestedFieldsTruncated = true; return; + } int index = 0; for (const auto& child : value.children()) { if (remainingChildren <= 0) { @@ -252,8 +258,25 @@ inline constexpr int kBrowserReportTreeScalarTextLimit = 160; static_cast(member.data_set_index + 1U)); if (member.value) { + const auto rowIndex = rows.size(); appendValue(appendValue, *member.value, static_cast(i), 0U, name, fullReference, shortened); + // ReasonForInclusion is optional wire evidence. Keep the exact + // bounded reason names only for the verified top-level member. + QString reasons; + bool ignoredTruncation = false; + for (const auto& reason : member.reason_for_inclusion.names) { + if (reasons.size() >= kBrowserReportTreeScalarTextLimit) break; + if (!reasons.isEmpty()) reasons += QStringLiteral(", "); + reasons += boundedReportMetadata(reason, + static_cast(std::max(0, + kBrowserReportTreeScalarTextLimit - int(reasons.size()))), + ignoredTruncation); + if (ignoredTruncation) break; + } + auto row = rows.at(rowIndex).toMap(); + row.insert(QStringLiteral("reason"), reasons); + rows[rowIndex] = row; } else { rows.push_back(QVariantMap{ {QStringLiteral("memberIndex"), static_cast(i)}, From e084dca37faa91877de692d71fe82e440c79fb3c Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 10:05:27 +0700 Subject: [PATCH 16/29] ux(workbench): inline verified q/t flag details and actual reason-for-inclusion per member --- apps/ied_simulator/qml/BrowserReportPane.qml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/apps/ied_simulator/qml/BrowserReportPane.qml b/apps/ied_simulator/qml/BrowserReportPane.qml index 89bc20f04..260f65961 100644 --- a/apps/ied_simulator/qml/BrowserReportPane.qml +++ b/apps/ied_simulator/qml/BrowserReportPane.qml @@ -429,10 +429,23 @@ Rectangle { color: root.theme.lineSoft } } + Label { + visible: treeRow.topLevel && treeRow.expanded && + Boolean(treeRow.modelData.reason) + Layout.fillWidth: true + leftPadding: 32 + topPadding: 4 + bottomPadding: 4 + color: root.theme.textSoft + font.pixelSize: 11 + text: "Reason for inclusion: " + (treeRow.modelData.reason || "") + } BrowserEngineeringEvidence { theme: root.theme + // Native tree row carries exactly this member's typed decoder + // evidence, so quality flags/time-quality stay visible on click. engineeringEvidence: treeRow.topLevel && treeRow.expanded - ? root.selectedReportMemberEvidence() : ({}) + ? treeRow.modelData.typedEvidence || ({}) : ({}) } } From b6ad092c5129096a2ea403a06892c3d61591a9fd Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 10:05:42 +0700 Subject: [PATCH 17/29] test(workbench): retain verified Quality flags, GI reason and deep tree truncation --- apps/ied_simulator/src/ied_report_workspace_qa.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/apps/ied_simulator/src/ied_report_workspace_qa.cpp b/apps/ied_simulator/src/ied_report_workspace_qa.cpp index 01e67b26e..e4f2239b1 100644 --- a/apps/ied_simulator/src/ied_report_workspace_qa.cpp +++ b/apps/ied_simulator/src/ied_report_workspace_qa.cpp @@ -40,6 +40,7 @@ bool verifyReadableReportTreeProjection() { mms::MmsReportValue quality; quality.data_set_index = 1U; quality.data_reference = "IED/LLN0.Pos.q"; + quality.reason_for_inclusion.names.push_back("general-interrogation"); quality.value = mms::MmsDataValue::bit_string(3U, std::span{goodQuality}); mms::MmsReportValue notRead; @@ -62,6 +63,9 @@ bool verifyReadableReportTreeProjection() { nested.value("depth").toInt() != 2 || q.value("name").toString() != QStringLiteral("LLN0.Pos.q") || q.value("value").toString() != QStringLiteral("good") || + q.value("reason").toString() != QStringLiteral("general-interrogation") || + q.value("typedEvidence").toMap().value("kind").toString() != QStringLiteral("quality") || + q.value("typedEvidence").toMap().value("rows").toList().size() < 10 || unread.value("received").toBool() || unread.value("value").toString() != QStringLiteral("Read failed")) return false; @@ -77,6 +81,16 @@ bool verifyReadableReportTreeProjection() { if (!truncated || limited.size() != arstack::iedsim::kBrowserReportTreeChildLimit + 1) return false; + // Recursion must not imply completeness at the configured depth cap. + auto deep = mms::MmsDataValue::boolean(true); + for (unsigned i = 0; i <= arstack::iedsim::kBrowserReportTreeDepthLimit; ++i) + deep = mms::MmsDataValue::structure({std::move(deep)}); + mms::MmsReportValue nested; + nested.data_reference = "IED/LLN0.Nested"; + nested.value = std::move(deep); + truncated = false; + const auto deepRows = projectBrowserReportTree({nested}, &truncated); + if (!truncated || deepRows.isEmpty()) return false; // Shortened references must not be used to infer q/t semantic identity. mms::MmsReportValue ambiguous; ambiguous.data_reference = std::string(1500U, 'X') + ".q"; From a297b24c137fdd0d21f4f2d5730b9cc44ea9314f Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 10:05:56 +0700 Subject: [PATCH 18/29] test(workbench): guard preserved q/t details, member reasons and depth-limit flags --- apps/ied_simulator/test_browser_convergence.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/ied_simulator/test_browser_convergence.py b/apps/ied_simulator/test_browser_convergence.py index d7569cd9d..a54989615 100644 --- a/apps/ied_simulator/test_browser_convergence.py +++ b/apps/ied_simulator/test_browser_convergence.py @@ -232,6 +232,10 @@ 'decodeEngineeringEvidence(fullReference, value)', 'depth == 0U && !isContainer && !fullReferenceShortened', 'if (remainingChildren <= 0) {', + 'depth >= kBrowserReportTreeDepthLimit', + '*nestedFieldsTruncated = true', + 'QStringLiteral("reason")', + 'QStringLiteral("typedEvidence")', ): if token not in projection_header: raise SystemExit("BROWSER_REPORT_TYPED_TREE_FAIL missing=" + token) @@ -249,6 +253,8 @@ 'root.latestReport.valueTree', 'treeRow.modelData.memberIndex', 'treeRow.modelData.value', + 'treeRow.modelData.typedEvidence', + 'Reason for inclusion:', 'visible: topLevel || expanded', 'property bool triggerExpanded: false', 'property bool optionalExpanded: false', From 4e84883458f61b0a8f69e81c4cf587d191ffd72f Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 10:12:52 +0700 Subject: [PATCH 19/29] fix(workbench): correct native report tree QA variable shadowing found by compiler --- apps/ied_simulator/src/ied_report_workspace_qa.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/ied_simulator/src/ied_report_workspace_qa.cpp b/apps/ied_simulator/src/ied_report_workspace_qa.cpp index e4f2239b1..5195ada8e 100644 --- a/apps/ied_simulator/src/ied_report_workspace_qa.cpp +++ b/apps/ied_simulator/src/ied_report_workspace_qa.cpp @@ -51,7 +51,7 @@ bool verifyReadableReportTreeProjection() { if (rows.size() != 7) return false; // struct, 3 children + 1 array item, q, unread const auto parent = rows.at(0).toMap(); const auto child = rows.at(1).toMap(); - const auto nested = rows.at(4).toMap(); + const auto nestedChild = rows.at(4).toMap(); const auto q = rows.at(5).toMap(); const auto unread = rows.at(6).toMap(); if (parent.value("name").toString() != QStringLiteral("LLN0.Pos") || @@ -60,7 +60,7 @@ bool verifyReadableReportTreeProjection() { child.value("name").toString() != QStringLiteral("Field 1") || child.value("value").toString() != QStringLiteral("false") || child.value("depth").toInt() != 1 || - nested.value("depth").toInt() != 2 || + nestedChild.value("depth").toInt() != 2 || q.value("name").toString() != QStringLiteral("LLN0.Pos.q") || q.value("value").toString() != QStringLiteral("good") || q.value("reason").toString() != QStringLiteral("general-interrogation") || From 28ee37e909f14e474fea64b1ff347d72c829b8b8 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 10:19:17 +0700 Subject: [PATCH 20/29] fix(workbench): retain GI/inclusion reason even on failed MMS report member --- .../src/BrowserReportMemberProjection.hpp | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/apps/ied_simulator/src/BrowserReportMemberProjection.hpp b/apps/ied_simulator/src/BrowserReportMemberProjection.hpp index 8e740a848..885ac2cfc 100644 --- a/apps/ied_simulator/src/BrowserReportMemberProjection.hpp +++ b/apps/ied_simulator/src/BrowserReportMemberProjection.hpp @@ -257,26 +257,10 @@ inline constexpr int kBrowserReportTreeScalarTextLimit = 160; if (name.isEmpty()) name = QStringLiteral("Member %1").arg( static_cast(member.data_set_index + 1U)); + const auto rowIndex = rows.size(); if (member.value) { - const auto rowIndex = rows.size(); appendValue(appendValue, *member.value, static_cast(i), 0U, name, fullReference, shortened); - // ReasonForInclusion is optional wire evidence. Keep the exact - // bounded reason names only for the verified top-level member. - QString reasons; - bool ignoredTruncation = false; - for (const auto& reason : member.reason_for_inclusion.names) { - if (reasons.size() >= kBrowserReportTreeScalarTextLimit) break; - if (!reasons.isEmpty()) reasons += QStringLiteral(", "); - reasons += boundedReportMetadata(reason, - static_cast(std::max(0, - kBrowserReportTreeScalarTextLimit - int(reasons.size()))), - ignoredTruncation); - if (ignoredTruncation) break; - } - auto row = rows.at(rowIndex).toMap(); - row.insert(QStringLiteral("reason"), reasons); - rows[rowIndex] = row; } else { rows.push_back(QVariantMap{ {QStringLiteral("memberIndex"), static_cast(i)}, @@ -289,7 +273,25 @@ inline constexpr int kBrowserReportTreeScalarTextLimit = 160; {QStringLiteral("received"), false} }); } + // Preserve ReasonForInclusion even for a failed MMS AccessResult. + // ReasonForInclusion is optional wire evidence. Keep the exact + // bounded reason names only for the verified top-level member. + QString reasons; + bool ignoredTruncation = false; + for (const auto& reason : member.reason_for_inclusion.names) { + if (reasons.size() >= kBrowserReportTreeScalarTextLimit) break; + if (!reasons.isEmpty()) reasons += QStringLiteral(", "); + reasons += boundedReportMetadata(reason, + static_cast(std::max(0, + kBrowserReportTreeScalarTextLimit - int(reasons.size()))), + ignoredTruncation); + if (ignoredTruncation) break; + } + auto row = rows.at(rowIndex).toMap(); + row.insert(QStringLiteral("reason"), reasons); + rows[rowIndex] = row; + } return rows; } -} // namespace arstack::iedsim +} // namespace arstack::iedsim \ No newline at end of file From 2ed5668bb97e67855993ff603459a3fa834a6af9 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 10:19:35 +0700 Subject: [PATCH 21/29] test(workbench): verify inclusion reason survives a failed MMS AccessResult --- apps/ied_simulator/src/ied_report_workspace_qa.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/ied_simulator/src/ied_report_workspace_qa.cpp b/apps/ied_simulator/src/ied_report_workspace_qa.cpp index 5195ada8e..14dfb7306 100644 --- a/apps/ied_simulator/src/ied_report_workspace_qa.cpp +++ b/apps/ied_simulator/src/ied_report_workspace_qa.cpp @@ -46,6 +46,7 @@ bool verifyReadableReportTreeProjection() { mms::MmsReportValue notRead; notRead.data_set_index = 2U; notRead.data_reference = "IED/LLN0.NotRead"; + notRead.reason_for_inclusion.names.push_back("data-change"); const auto rows = projectBrowserReportTree({structured, quality, notRead}); if (rows.size() != 7) return false; // struct, 3 children + 1 array item, q, unread @@ -67,6 +68,7 @@ bool verifyReadableReportTreeProjection() { q.value("typedEvidence").toMap().value("kind").toString() != QStringLiteral("quality") || q.value("typedEvidence").toMap().value("rows").toList().size() < 10 || unread.value("received").toBool() || + unread.value("reason").toString() != QStringLiteral("data-change") || unread.value("value").toString() != QStringLiteral("Read failed")) return false; From 8831b2e51eaeae0cb0a29bf6e74eb73f5dda5c99 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 10:23:42 +0700 Subject: [PATCH 22/29] fix(workbench): only show legacy typed-evidence budget warning on legacy report view --- apps/ied_simulator/qml/BrowserReportPane.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/ied_simulator/qml/BrowserReportPane.qml b/apps/ied_simulator/qml/BrowserReportPane.qml index 260f65961..fa37a3d82 100644 --- a/apps/ied_simulator/qml/BrowserReportPane.qml +++ b/apps/ied_simulator/qml/BrowserReportPane.qml @@ -795,6 +795,8 @@ Rectangle { } Label { visible: root.hasLastReport && + (!root.latestReport.valueTree || + root.latestReport.valueTree.length === 0) && root.latestReport.valueEvidenceTruncated === true Layout.fillWidth: true leftPadding: 9 From 02b82ec2a9f83a0332c5f6df2d53542e180e7d07 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 10:23:58 +0700 Subject: [PATCH 23/29] test(workbench): no misleading q/t omission warning with complete typed tree --- apps/ied_simulator/test_browser_convergence.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/ied_simulator/test_browser_convergence.py b/apps/ied_simulator/test_browser_convergence.py index a54989615..c25b3afab 100644 --- a/apps/ied_simulator/test_browser_convergence.py +++ b/apps/ied_simulator/test_browser_convergence.py @@ -261,9 +261,17 @@ 'property bool informationExpanded: false', 'onSelectedRcbRowChanged:', 'root.latestReport.treeNestedTruncated === true', + 'root.latestReport.valueEvidenceTruncated === true', ): if token not in report_inspector: raise SystemExit("BROWSER_REPORT_TYPED_TREE_FAIL QML missing=" + token) +# Do not tell operators q/t flags are missing merely because the old 96 +# row evidence budget was exhausted; the new native tree retains direct q/t. +evidence_warning = report_inspector.split( + 'root.latestReport.valueEvidenceTruncated === true', 1)[0] +if not evidence_warning.endswith( + 'root.latestReport.valueTree.length === 0) &&\\n '): + raise SystemExit("BROWSER_REPORT_TYPED_TREE_FAIL legacy q/t warning leaks into typed tree") if 'text: root.memberValue(modelData)' in report_inspector: raise SystemExit("BROWSER_REPORT_TYPED_TREE_FAIL concatenated-string value in active inspector") if report_controller.count('projectBrowserReportTree(') != 1: From 149f9821dc7abb4398451a0ef3066c1b9931dfd2 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 10:24:17 +0700 Subject: [PATCH 24/29] test(workbench): make tree warning gate whitespace-agnostic --- apps/ied_simulator/test_browser_convergence.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/ied_simulator/test_browser_convergence.py b/apps/ied_simulator/test_browser_convergence.py index c25b3afab..17a7a4f79 100644 --- a/apps/ied_simulator/test_browser_convergence.py +++ b/apps/ied_simulator/test_browser_convergence.py @@ -269,8 +269,8 @@ # row evidence budget was exhausted; the new native tree retains direct q/t. evidence_warning = report_inspector.split( 'root.latestReport.valueEvidenceTruncated === true', 1)[0] -if not evidence_warning.endswith( - 'root.latestReport.valueTree.length === 0) &&\\n '): +if ('(!root.latestReport.valueTree ||' not in evidence_warning[-220:] or + 'root.latestReport.valueTree.length === 0) &&' not in evidence_warning[-220:]): raise SystemExit("BROWSER_REPORT_TYPED_TREE_FAIL legacy q/t warning leaks into typed tree") if 'text: root.memberValue(modelData)' in report_inspector: raise SystemExit("BROWSER_REPORT_TYPED_TREE_FAIL concatenated-string value in active inspector") From e145be7482b1ed4ebbd2f03b3d42799afc0c21bf Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 10:27:41 +0700 Subject: [PATCH 25/29] fix(workbench): explicitly mark typed q/t evidence omitted by shortened member identity --- .../src/BrowserReportMemberProjection.hpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/ied_simulator/src/BrowserReportMemberProjection.hpp b/apps/ied_simulator/src/BrowserReportMemberProjection.hpp index 885ac2cfc..fd3f75420 100644 --- a/apps/ied_simulator/src/BrowserReportMemberProjection.hpp +++ b/apps/ied_simulator/src/BrowserReportMemberProjection.hpp @@ -175,8 +175,10 @@ inline constexpr int kBrowserReportTreeScalarTextLimit = 160; [[nodiscard]] inline QVariantList projectBrowserReportTree( const std::vector& members, - bool* nestedFieldsTruncated = nullptr) { + bool* nestedFieldsTruncated = nullptr, + bool* typedEvidenceOmitted = nullptr) { if (nestedFieldsTruncated) *nestedFieldsTruncated = false; + if (typedEvidenceOmitted) *typedEvidenceOmitted = false; QVariantList rows; const auto visibleCount = std::min( members.size(), static_cast(kBrowserReportVisibleMemberLimit)); @@ -249,6 +251,12 @@ inline constexpr int kBrowserReportTreeScalarTextLimit = 160; bool shortened = false; const QString fullReference = boundedReportMetadata( *source, kBrowserReportTextLimit, shortened); + // A truncated q/t identity cannot be safely fed to the shared typed + // decoder; retain a specific omission indicator instead of showing a + // misleading good/unknown interpretation or suppressing all warnings. + if (shortened && typedEvidenceOmitted && member.value && + hasReportTypedLeaf(*source, member.value->kind())) + *typedEvidenceOmitted = true; QString name = fullReference; // Prefer compact canonical IEC paths while keeping full reference in // the per-row tooltip and never forging a functional constraint. From a0fe442788cea4b1492a857aa269f7ad6d6d94d0 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 10:27:52 +0700 Subject: [PATCH 26/29] fix(workbench): expose only genuine typed evidence omission on active native report tree --- apps/ied_simulator/src/MmsReportController.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/ied_simulator/src/MmsReportController.cpp b/apps/ied_simulator/src/MmsReportController.cpp index b0af52f39..344ebd59b 100644 --- a/apps/ied_simulator/src/MmsReportController.cpp +++ b/apps/ied_simulator/src/MmsReportController.cpp @@ -538,10 +538,12 @@ ReportUi buildSubscriptionUi( // No second decoder, protocol store or UI-owned report mutation. if (ui.reports.isEmpty()) { bool nestedTruncated = false; + bool typedEvidenceOmitted = false; map.insert(QStringLiteral("valueTree"), arstack::iedsim::projectBrowserReportTree( - frame.values, &nestedTruncated)); + frame.values, &nestedTruncated, &typedEvidenceOmitted)); map.insert(QStringLiteral("treeNestedTruncated"), nestedTruncated); + map.insert(QStringLiteral("treeTypedEvidenceOmitted"), typedEvidenceOmitted); } map.insert(QStringLiteral("valueEvidence"), projected.evidence); map.insert(QStringLiteral("valueEvidenceTruncated"), projected.evidenceTruncated); From f8e66f2f5550ffda7eb6385619c7c222ac4a9f6f Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 10:28:12 +0700 Subject: [PATCH 27/29] ux(workbench): inform operator only when report q/t identity prevents typed decode --- apps/ied_simulator/qml/BrowserReportPane.qml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/apps/ied_simulator/qml/BrowserReportPane.qml b/apps/ied_simulator/qml/BrowserReportPane.qml index fa37a3d82..476df00cb 100644 --- a/apps/ied_simulator/qml/BrowserReportPane.qml +++ b/apps/ied_simulator/qml/BrowserReportPane.qml @@ -793,6 +793,17 @@ Rectangle { root.latestReport.valueTree.length === 0) ? root.selectedReportMemberEvidence() : ({}) } + Label { + visible: root.hasLastReport && + root.latestReport.treeTypedEvidenceOmitted === true + Layout.fillWidth: true + leftPadding: 9 + topPadding: 3 + bottomPadding: 3 + text: "Quality/TimeQuality details withheld: member identity was truncated." + color: theme.muted + font.pixelSize: 11 + } Label { visible: root.hasLastReport && (!root.latestReport.valueTree || From 2bf4a10fb1ab610ac42282b135c32d8dc9ac6a84 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 10:28:24 +0700 Subject: [PATCH 28/29] test(workbench): distinguish real q/t omission from unchanged valid Quality evidence --- apps/ied_simulator/src/ied_report_workspace_qa.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/ied_simulator/src/ied_report_workspace_qa.cpp b/apps/ied_simulator/src/ied_report_workspace_qa.cpp index 14dfb7306..8cd55e94d 100644 --- a/apps/ied_simulator/src/ied_report_workspace_qa.cpp +++ b/apps/ied_simulator/src/ied_report_workspace_qa.cpp @@ -98,9 +98,17 @@ bool verifyReadableReportTreeProjection() { ambiguous.data_reference = std::string(1500U, 'X') + ".q"; ambiguous.value = mms::MmsDataValue::bit_string(3U, std::span{goodQuality}); - const auto raw = projectBrowserReportTree({ambiguous}); - if (raw.isEmpty() || raw.first().toMap().value("value").toString() - == QStringLiteral("good")) + bool qOmitted = false; + const auto raw = projectBrowserReportTree({ambiguous}, nullptr, &qOmitted); + if (raw.isEmpty() || !qOmitted || + raw.first().toMap().value("value").toString() + == QStringLiteral("good")) + return false; + qOmitted = false; + const auto directQuality = projectBrowserReportTree({quality}, nullptr, &qOmitted); + if (directQuality.isEmpty() || qOmitted || + directQuality.first().toMap().value("value").toString() + != QStringLiteral("good")) return false; return true; } From 3d8edd4449ecb9371c85a7f7f86c8905d919dbb7 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 10:28:36 +0700 Subject: [PATCH 29/29] test(workbench): guard truthful q/t omission only on shortened actual member identity --- apps/ied_simulator/test_browser_convergence.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/ied_simulator/test_browser_convergence.py b/apps/ied_simulator/test_browser_convergence.py index 17a7a4f79..0570ead11 100644 --- a/apps/ied_simulator/test_browser_convergence.py +++ b/apps/ied_simulator/test_browser_convergence.py @@ -228,6 +228,8 @@ 'kBrowserReportTreeScalarTextLimit = 160', 'projectBrowserReportTree(', 'bool* nestedFieldsTruncated = nullptr', + 'bool* typedEvidenceOmitted = nullptr', + 'if (shortened && typedEvidenceOmitted && member.value', 'QStringLiteral("Field %1")', 'decodeEngineeringEvidence(fullReference, value)', 'depth == 0U && !isContainer && !fullReferenceShortened', @@ -243,6 +245,7 @@ 'if (ui.reports.isEmpty()) {', 'QStringLiteral("valueTree")', 'QStringLiteral("treeNestedTruncated")', + 'QStringLiteral("treeTypedEvidenceOmitted")', 'projectBrowserReportTree(', ): if token not in report_controller: @@ -261,6 +264,7 @@ 'property bool informationExpanded: false', 'onSelectedRcbRowChanged:', 'root.latestReport.treeNestedTruncated === true', + 'root.latestReport.treeTypedEvidenceOmitted === true', 'root.latestReport.valueEvidenceTruncated === true', ): if token not in report_inspector: