From b3c2b457a98c43d105500cf05e23e459c136c4c7 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 18:30:10 +0700 Subject: [PATCH 01/27] feat(p0): port ARIEC61850 fail-closed exact SCL DataSet report binding --- .../src/BrowserReportSemanticBinding.hpp | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 apps/ied_simulator/src/BrowserReportSemanticBinding.hpp diff --git a/apps/ied_simulator/src/BrowserReportSemanticBinding.hpp b/apps/ied_simulator/src/BrowserReportSemanticBinding.hpp new file mode 100644 index 000000000..f0b7b0084 --- /dev/null +++ b/apps/ied_simulator/src/BrowserReportSemanticBinding.hpp @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "ariec61850/mms/live_model.hpp" +#include "ariec61850/mms/reporting.hpp" +#include +#include +#include +#include +#include +#include + +// Native port of ARIEC61850 MmsSemanticReportValueProjector's fail-closed +// invariants. Read-only, no additional protocol stack or MMS association. +namespace arstack::iedsim { +namespace semantic_mms = ar::iec61850::mms; +struct VerifiedReportLeaf final { + std::string reference; + const semantic_mms::MmsDataValue* value{}; +}; +inline bool semanticInside(const std::string_view leaf, + const std::string_view root) { + return leaf.size() > root.size() && leaf.substr(0,root.size()) == root && + leaf[root.size()] == '.'; +} +inline bool semanticKindCompatible(const semantic_mms::MmsDataValue& value, + const std::string_view basic) { + using K = semantic_mms::MmsDataKind; + if (basic == "BOOLEAN") return value.kind() == K::boolean; + if (basic == "Quality" || basic == "QUALITY") return value.kind() == K::bit_string; + if (basic == "Timestamp" || basic == "TIMESTAMP") return value.kind() == K::utc_time; + if (basic == "Enum" || basic == "ENUM" || basic == "INT32" || + basic == "INT8" || basic == "INT16") return value.kind() == K::integer; + if (basic == "FLOAT32" || basic == "FLOAT64") + return value.kind() == K::floating_point; + // Unknown/vendor type cannot authorize semantic labelling. + return false; +} +inline std::optional> bindFlatSclReportStructure( + const semantic_mms::MmsLiveModelDocument& model, + const std::string_view dataSet, + const semantic_mms::MmsReportValue& member) { + using namespace semantic_mms; + if (!member.value || member.value->kind() != MmsDataKind::structure) + return std::nullopt; + // SCL identity/model must be trusted and the FCDA directory must be + // uniquely bound to this DataSet and exact ordinal. Never infer by RptID. + if (model.identity.confidence != MmsLiveModelConfidence::exact || + model.source != "TrustedSclInitialSnapshot" || dataSet.empty()) + return std::nullopt; + const MmsLiveDataSet* chosen = nullptr; + for (const auto& ds : model.data_sets) { + if (ds.reference != dataSet) continue; + if (chosen) return std::nullopt; + chosen = &ds; + } + if (!chosen) return std::nullopt; + const MmsLiveDataSetMember* binding = nullptr; + for (const auto& fcda : chosen->members) { + if (fcda.index != member.data_set_index) continue; + if (binding) return std::nullopt; + binding = &fcda; + } + if (!binding || binding->confidence != MmsLiveModelConfidence::exact || + binding->reference.empty() || binding->functional_constraint.empty()) + return std::nullopt; + const auto& raw = member.data_reference; + const auto& dir = member.member; + const bool exactWireRef = raw == binding->reference || + (dir && dir->user_reference == binding->reference); + if (!exactWireRef) return std::nullopt; + if (dir && !dir->functional_constraint.empty() && + dir->functional_constraint != binding->functional_constraint) + return std::nullopt; + // Flat ST/CF/SG structures only. Nested, array, repeated or vendor + // layouts have no proven ordered schema here and remain positional. + const MmsLiveDataObject* object = nullptr; + for (const auto& ld : model.logical_devices) + for (const auto& ln : ld.logical_nodes) + for (const auto& candidate : ln.data_objects) + if (binding->reference == candidate.reference) { + if (object) return std::nullopt; + object = &candidate; + } + if (!object || object->confidence != MmsLiveModelConfidence::exact) + return std::nullopt; + std::vector attributes; + for (const auto& attribute : object->attributes) { + if (attribute.functional_constraint != binding->functional_constraint || + !semanticInside(attribute.object_reference, binding->reference)) + continue; + if (attribute.source != "TrustedSCL" || + attribute.type_confidence != MmsLiveModelConfidence::exact || + attribute.functional_constraint_confidence != MmsLiveModelConfidence::exact) + return std::nullopt; + // Only one direct segment is admissible; nested structures demand + // matching a hierarchical SCL schema, not guesswork. + const auto suffix = std::string_view{attribute.object_reference} + .substr(binding->reference.size() + 1); + if (suffix.empty() || suffix.find('.') != std::string_view::npos) + return std::nullopt; + attributes.push_back(&attribute); + } + const auto& children = member.value->children(); + if (attributes.empty() || attributes.size() != children.size() || + children.size() > 16U) return std::nullopt; + std::vector result; + result.reserve(children.size()); + for (std::size_t i = 0; i < children.size(); ++i) { + if (!semanticKindCompatible(children[i], attributes[i]->scl_basic_type)) + return std::nullopt; + if (std::any_of(result.begin(), result.end(), [&](const auto& leaf){ + return leaf.reference == attributes[i]->object_reference; })) + return std::nullopt; + result.push_back({attributes[i]->object_reference, &children[i]}); + } + return result; +} +} // namespace arstack::iedsim From 3afe9e9259f111faf49611db2697d7e55a4844ae Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 18:30:35 +0700 Subject: [PATCH 02/27] feat(p0): project model-verified report leaves with fail-closed positional fallback --- .../src/BrowserReportMemberProjection.hpp | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/apps/ied_simulator/src/BrowserReportMemberProjection.hpp b/apps/ied_simulator/src/BrowserReportMemberProjection.hpp index 99a664e9b..cb37a0be6 100644 --- a/apps/ied_simulator/src/BrowserReportMemberProjection.hpp +++ b/apps/ied_simulator/src/BrowserReportMemberProjection.hpp @@ -3,6 +3,7 @@ #include "MmsEngineeringEvidence.hpp" #include "BrowserBoundedMmsPreview.hpp" +#include "BrowserReportSemanticBinding.hpp" #include "ariec61850/mms/reporting.hpp" #include @@ -64,7 +65,9 @@ inline constexpr int kBrowserReportDetailChildren = 16; const std::vector& members, int& remainingTypedEvidence, const int requestedVisibleMembers = kBrowserReportVisibleMemberLimit, - const bool includeWireDetails = false) { + const bool includeWireDetails = false, + const mms::MmsLiveModelDocument* trustedModel = nullptr, + const std::string_view dataSetReference = {}) { BrowserReportMemberProjection projection; const int visibleLimit = std::clamp(requestedVisibleMembers, 0, kBrowserReportVisibleMemberLimit); const auto visibleCount = std::min(members.size(), static_cast(visibleLimit)); @@ -113,7 +116,25 @@ inline constexpr int kBrowserReportDetailChildren = 16; .arg(includeWireDetails && ordinal < kBrowserReportDetailMembers ? QStringLiteral(" — select for details") : QString{}); - if (includeWireDetails && ordinal < kBrowserReportDetailMembers) { + const auto verified = includeWireDetails && trustedModel + ? bindFlatSclReportStructure(*trustedModel, dataSetReference, member) + : std::nullopt; + if (verified) { + displayValue = QStringLiteral("SCL-verified structure · %1 field(s)") + .arg(static_cast(verified->size())); + for (const auto& leaf : *verified) { + const auto preview = previewMmsValue(*leaf.value, kBrowserReportTextLimit); + const auto full = QString::fromStdString(leaf.reference); + const auto dot = full.lastIndexOf(QLatin1Char('.')); + detailRows.push_back(QVariantMap{ + {QStringLiteral("label"), dot >= 0 ? full.mid(dot + 1) : full}, + {QStringLiteral("value"), QString::fromStdString(preview.text) + + (preview.truncated ? QStringLiteral(" … [truncated]") : QString{})}, + {QStringLiteral("authority"), QStringLiteral("SCL exact / MMS type checked")}, + {QStringLiteral("reference"), full}}); + if (preview.truncated) projection.textTruncated = true; + } + } else if (includeWireDetails && ordinal < kBrowserReportDetailMembers) { const auto count = std::min(children.size(), static_cast(kBrowserReportDetailChildren)); for (std::size_t child = 0; child < count; ++child) { From 21433d4fda69ac8fe7c4bba538be5abd16d5e4a4 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 18:30:55 +0700 Subject: [PATCH 03/27] feat(p0): bind report worker to generation-scoped canonical SCL semantic model --- .../ied_simulator/src/MmsReportController.cpp | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/apps/ied_simulator/src/MmsReportController.cpp b/apps/ied_simulator/src/MmsReportController.cpp index 758001e55..576c0d36e 100644 --- a/apps/ied_simulator/src/MmsReportController.cpp +++ b/apps/ied_simulator/src/MmsReportController.cpp @@ -491,7 +491,8 @@ struct ReportUi final { ReportUi buildSubscriptionUi( const mms::MmsReportSubscriptionSnapshot& subscription, - const bool active) { + const bool active, + const mms::MmsLiveModelDocument* semanticModel = nullptr) { ReportUi ui; ui.active = active; ui.receivedCount = subscription.received_reports; @@ -537,7 +538,7 @@ ReportUi buildSubscriptionUi( const auto projected = arstack::iedsim::projectBrowserReportMembers( frame.values, typedEvidenceBudget, arstack::iedsim::kBrowserReportVisibleMemberLimit, - includeWireDetails); + includeWireDetails, semanticModel, frame.header.data_set_reference); map.insert(QStringLiteral("values"), projected.values); map.insert(QStringLiteral("valueEvidence"), projected.evidence); map.insert(QStringLiteral("valueDetails"), projected.details); @@ -557,19 +558,21 @@ ReportUi buildSubscriptionUi( return ui; } -ReportUi buildReportUi(const mms::MmsStaticReportSessionSnapshot& snapshot) { +ReportUi buildReportUi(const mms::MmsStaticReportSessionSnapshot& snapshot, + const mms::MmsLiveModelDocument* semanticModel = nullptr) { if (!snapshot.subscription) { ReportUi ui; ui.active = snapshot.active; return ui; } - return buildSubscriptionUi(*snapshot.subscription, snapshot.active); + return buildSubscriptionUi(*snapshot.subscription, snapshot.active, semanticModel); } -ReportUi buildReportUi(const mms::MmsReportSubscriptionSnapshot& snapshot) { +ReportUi buildReportUi(const mms::MmsReportSubscriptionSnapshot& snapshot, + const mms::MmsLiveModelDocument* semanticModel = nullptr) { return buildSubscriptionUi( snapshot, - snapshot.state == mms::MmsReportSubscriptionState::active); + snapshot.state == mms::MmsReportSubscriptionState::active, semanticModel); } } // namespace @@ -584,6 +587,9 @@ struct MmsReportController::WorkerState final { // implicitly shared containers keep this cheap to retain; deep compare // must never run on the QML/controller thread. std::optional lastPublishedReportUi; + // Association-generation-owned canonical SCL model, captured on GUI thread + // before worker launch; no QObject or model is accessed cross-thread. + std::shared_ptr semanticModel; }; MmsReportController::MmsReportController(QObject* parent) @@ -786,6 +792,7 @@ bool MmsReportController::connectToIed() { ioPool_.start([self, worker, stop, generation, requestedHost, requestedPort, contextModel] { try { worker->lastPublishedReportUi.reset(); + worker->semanticModel = contextModel; if (worker->report) worker->report->stop(); if (worker->authoredReport) worker->authoredReport->stop(); worker->report.reset(); @@ -1779,11 +1786,11 @@ bool MmsReportController::disableSelected() { ReportUi ui; if (worker->report) { worker->report->stop(stop->get_token()); - ui = buildReportUi(worker->report->snapshot()); + ui = buildReportUi(worker->report->snapshot(), worker->semanticModel.get()); if (!ui.cleanupRequired) worker->report.reset(); } else if (worker->authoredReport) { worker->authoredReport->stop(stop->get_token()); - ui = buildReportUi(worker->authoredReport->snapshot()); + ui = buildReportUi(worker->authoredReport->snapshot(), worker->semanticModel.get()); if (!ui.cleanupRequired) worker->authoredReport.reset(); } else { throw std::runtime_error("No report subscription is active."); @@ -1850,12 +1857,12 @@ bool MmsReportController::retryCleanup() { ReportUi ui; if (worker->report) { worker->report->stop(stop->get_token()); - ui = buildReportUi(worker->report->snapshot()); + ui = buildReportUi(worker->report->snapshot(), worker->semanticModel.get()); if (!ui.cleanupRequired) worker->report.reset(); } else if (worker->authoredReport) { static_cast(worker->authoredReport->retry_cleanup( stop->get_token())); - ui = buildReportUi(worker->authoredReport->snapshot()); + ui = buildReportUi(worker->authoredReport->snapshot(), worker->semanticModel.get()); if (!ui.cleanupRequired) worker->authoredReport.reset(); } else { throw std::runtime_error( @@ -1945,8 +1952,8 @@ void MmsReportController::schedulePoll() { return; } const ReportUi ui = staticActive - ? buildReportUi(worker->report->snapshot()) - : buildReportUi(worker->authoredReport->snapshot()); + ? buildReportUi(worker->report->snapshot(), worker->semanticModel.get()) + : buildReportUi(worker->authoredReport->snapshot(), worker->semanticModel.get()); // Compare complete bounded report presentation on the existing // serial I/O worker, never on the GUI thread. Counter-only checks // would lose event-only and retained-frame evidence changes. @@ -1997,10 +2004,10 @@ void MmsReportController::schedulePoll() { ReportUi ui; if (worker->report) { worker->report->stop(); - ui = buildReportUi(worker->report->snapshot()); + ui = buildReportUi(worker->report->snapshot(), worker->semanticModel.get()); } else if (worker->authoredReport) { worker->authoredReport->stop(); - ui = buildReportUi(worker->authoredReport->snapshot()); + ui = buildReportUi(worker->authoredReport->snapshot(), worker->semanticModel.get()); } if (self) { const auto message = QString::fromUtf8(exception.what()); From bb2a1e4f71db19f379c4a6c3feb145ebea087680 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 18:31:52 +0700 Subject: [PATCH 04/27] safety(p0): reject contradictory report references and ambiguous identical-type ordering --- .../src/BrowserReportSemanticBinding.hpp | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/apps/ied_simulator/src/BrowserReportSemanticBinding.hpp b/apps/ied_simulator/src/BrowserReportSemanticBinding.hpp index f0b7b0084..45b0cce37 100644 --- a/apps/ied_simulator/src/BrowserReportSemanticBinding.hpp +++ b/apps/ied_simulator/src/BrowserReportSemanticBinding.hpp @@ -65,9 +65,13 @@ inline std::optional> bindFlatSclReportStructure return std::nullopt; const auto& raw = member.data_reference; const auto& dir = member.member; - const bool exactWireRef = raw == binding->reference || - (dir && dir->user_reference == binding->reference); - if (!exactWireRef) return std::nullopt; + // Any supplied wire source must agree; do not let a directory alias + // override a contradictory explicit Report DataRef. + if (!raw.empty() && raw != binding->reference) return std::nullopt; + if (dir && !dir->user_reference.empty() && + dir->user_reference != binding->reference) return std::nullopt; + if (raw.empty() && (!dir || dir->user_reference.empty())) + return std::nullopt; if (dir && !dir->functional_constraint.empty() && dir->functional_constraint != binding->functional_constraint) return std::nullopt; @@ -103,6 +107,13 @@ inline std::optional> bindFlatSclReportStructure const auto& children = member.value->children(); if (attributes.empty() || attributes.size() != children.size() || children.size() > 16U) return std::nullopt; + // Positional order is not independently proven by matching count alone. + // Reject repeated scalar types, where a vendor permutation would look + // valid and produce deceptively named engineering values. + for (std::size_t i = 0; i < attributes.size(); ++i) + for (std::size_t j = i + 1; j < attributes.size(); ++j) + if (attributes[i]->scl_basic_type == attributes[j]->scl_basic_type) + return std::nullopt; std::vector result; result.reserve(children.size()); for (std::size_t i = 0; i < children.size(); ++i) { From 8d420ecd43af7827d9a8f9afd96919d31cfed78f Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 18:32:52 +0700 Subject: [PATCH 05/27] test(p0): exercise exact SCL-bound report leaves and six fail-closed negatives --- .../src/ied_report_workspace_qa.cpp | 73 +++++++++++++++++++ 1 file changed, 73 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..675532dae 100644 --- a/apps/ied_simulator/src/ied_report_workspace_qa.cpp +++ b/apps/ied_simulator/src/ied_report_workspace_qa.cpp @@ -2,6 +2,7 @@ #include "IedFleetController.hpp" #include "IedReportControlManifest.hpp" +#include "BrowserReportSemanticBinding.hpp" #include "MmsReportController.hpp" #include @@ -170,12 +171,84 @@ bool reportProjectionPresent(const MmsReportController& reports) { frame.contains(QStringLiteral("entryId")) && frame.contains(QStringLiteral("overflow")); } +bool verifySemanticReportBinding() { + namespace mms = ar::iec61850::mms; + using arstack::iedsim::bindFlatSclReportStructure; + mms::MmsLiveModelDocument model; + model.source = "TrustedSclInitialSnapshot"; + model.identity.confidence = mms::MmsLiveModelConfidence::exact; + mms::MmsLiveDataSet dataset; + dataset.reference = "IEDLD/LLN0.ds"; + mms::MmsLiveDataSetMember binding; + binding.index = 0U; + binding.reference = "IEDLD/XCBR1.Pos"; + binding.functional_constraint = "ST"; + dataset.members.push_back(binding); + model.data_sets.push_back(dataset); + mms::MmsLiveLogicalDevice ld; + mms::MmsLiveLogicalNode ln; + mms::MmsLiveDataObject object; + object.reference = binding.reference; + object.confidence = mms::MmsLiveModelConfidence::exact; + for (const auto& [name, type] : { + std::pair{"stVal", "Enum"}, + std::pair{"q", "Quality"}}) { + mms::MmsLiveDataAttribute attr; + attr.object_reference = binding.reference + "." + name; + attr.attribute_path = name; + attr.functional_constraint = "ST"; + attr.source = "TrustedSCL"; + attr.type_confidence = mms::MmsLiveModelConfidence::exact; + attr.functional_constraint_confidence = mms::MmsLiveModelConfidence::exact; + attr.scl_basic_type = type; + object.attributes.push_back(std::move(attr)); + } + ln.data_objects.push_back(std::move(object)); + ld.logical_nodes.push_back(std::move(ln)); + model.logical_devices.push_back(std::move(ld)); + const std::array quality{0x00U, 0x00U}; + mms::MmsReportValue value; + value.data_set_index = 0U; + value.data_reference = binding.reference; + value.value = mms::MmsDataValue::structure({ + mms::MmsDataValue::integer(2), + mms::MmsDataValue::bit_string(0U, quality)}); + auto check = [&](const bool expected) { + const auto result = bindFlatSclReportStructure(model, dataset.reference, value); + return result.has_value() == expected; + }; + if (!check(true)) return false; + value.data_reference = "OtherIED/XCBR1.Pos"; + if (!check(false)) return false; + value.data_reference = binding.reference; + value.data_set_index = 3U; + if (!check(false)) return false; + value.data_set_index = 0U; + model.identity.confidence = mms::MmsLiveModelConfidence::high; + if (!check(false)) return false; + model.identity.confidence = mms::MmsLiveModelConfidence::exact; + value.value = mms::MmsDataValue::structure({mms::MmsDataValue::integer(2)}); + if (!check(false)) return false; + value.value = mms::MmsDataValue::structure({ + mms::MmsDataValue::integer(2), mms::MmsDataValue::integer(3)}); + if (!check(false)) return false; + value.value = mms::MmsDataValue::structure({ + mms::MmsDataValue::integer(2), mms::MmsDataValue::bit_string(0U, quality)}); + model.logical_devices[0].logical_nodes[0].data_objects[0].attributes[1].scl_basic_type = "Enum"; + if (!check(false)) return false; + return true; +} } // namespace int main(int argc, char* argv[]) { qputenv("QT_QPA_PLATFORM", "offscreen"); qputenv("ARSTACK_IEDSIM_QA", "1"); QGuiApplication app(argc, argv); + if (!verifySemanticReportBinding()) { + qCritical() << "REPORTS_SEMANTIC_BINDING_FAIL"; + return 42; + } + qInfo() << "REPORTS_SEMANTIC_BINDING_PASS exact=1 mismatch=6"; if (argc != 2) { qCritical() << "usage: ied_report_workspace_qa "; return 2; From e4f0e5bc956088b54c416352c64d164bcd27f3d4 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 18:33:07 +0700 Subject: [PATCH 06/27] ui(p0): distinguish model-bound semantic report leaves from positional fallback --- apps/ied_simulator/qml/BrowserReportPane.qml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/ied_simulator/qml/BrowserReportPane.qml b/apps/ied_simulator/qml/BrowserReportPane.qml index 359b6d1ef..d8b2c868b 100644 --- a/apps/ied_simulator/qml/BrowserReportPane.qml +++ b/apps/ied_simulator/qml/BrowserReportPane.qml @@ -682,7 +682,10 @@ Rectangle { Layout.fillWidth: true } Label { - text: "Positional · not SCL-labelled" + text: root.selectedReportMemberDetails().length > 0 + && root.selectedReportMemberDetails()[0].authority + ? "SCL verified · MMS type checked" + : "Positional · not SCL-labelled" font.pixelSize: 10 color: theme.muted } From f29e8ba2e7cce9c12b067265c74448f8a755db1d Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 18:33:31 +0700 Subject: [PATCH 07/27] test(p0): guard authoritative SCL semantic projection and negative binding rules --- .../ied_simulator/test_browser_convergence.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/apps/ied_simulator/test_browser_convergence.py b/apps/ied_simulator/test_browser_convergence.py index 41fadf5b2..c7ffb40f6 100644 --- a/apps/ied_simulator/test_browser_convergence.py +++ b/apps/ied_simulator/test_browser_convergence.py @@ -284,6 +284,29 @@ if report_inspector.count("BrowserEngineeringEvidence {") != 1: raise SystemExit("BROWSER_REPORT_INLINE_TREE_FAIL duplicate or detached q/t inspector") +# P0 semantic projection borrows ARIEC61850 exact DataSet/FCDA/type gates. +semantic = (workspace_path.parent.parent / "src" / "BrowserReportSemanticBinding.hpp").read_text(encoding="utf-8") +for token in ( + "model.identity.confidence != MmsLiveModelConfidence::exact", + 'model.source != "TrustedSclInitialSnapshot"', + "fcda.index != member.data_set_index", + "dir->user_reference != binding->reference", + "attribute.functional_constraint != binding->functional_constraint", + "attributes.size() != children.size()", + "semanticKindCompatible(children[i], attributes[i]->scl_basic_type)", + "attributes[i]->scl_basic_type == attributes[j]->scl_basic_type", +): + if token not in semantic: + raise SystemExit("BROWSER_SEMANTIC_FAIL missing fail-closed binding=" + token) +for token in ( + "worker->semanticModel = contextModel;", + "semanticModel, frame.header.data_set_reference", +): + if token not in report_controller: + raise SystemExit("BROWSER_SEMANTIC_FAIL missing generation-owned runtime wiring=" + token) +if "SCL verified · MMS type checked" not in report_inspector: + raise SystemExit("BROWSER_SEMANTIC_FAIL missing semantic/fallback provenance badge") + value_preview = (workspace_path.parent.parent / "src" / "BrowserBoundedMmsPreview.hpp").read_text(encoding="utf-8") for token in [ 'BoundedMmsPreview', From 307ae31104e0b8f3ae16fcbe129654b2e7745f0c Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 18:35:07 +0700 Subject: [PATCH 08/27] feat(p0): reuse canonical Quality/TimeQuality decoder for verified report child values --- apps/ied_simulator/src/BrowserReportMemberProjection.hpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/ied_simulator/src/BrowserReportMemberProjection.hpp b/apps/ied_simulator/src/BrowserReportMemberProjection.hpp index cb37a0be6..1d817b83e 100644 --- a/apps/ied_simulator/src/BrowserReportMemberProjection.hpp +++ b/apps/ied_simulator/src/BrowserReportMemberProjection.hpp @@ -126,10 +126,15 @@ inline constexpr int kBrowserReportDetailChildren = 16; const auto preview = previewMmsValue(*leaf.value, kBrowserReportTextLimit); const auto full = QString::fromStdString(leaf.reference); const auto dot = full.lastIndexOf(QLatin1Char('.')); + const auto engineering = decodeEngineeringEvidence(full, *leaf.value); + const QString displayed = engineering.isEmpty() + ? QString::fromStdString(preview.text) + : engineering.value(QStringLiteral("summary")).toString(); detailRows.push_back(QVariantMap{ {QStringLiteral("label"), dot >= 0 ? full.mid(dot + 1) : full}, - {QStringLiteral("value"), QString::fromStdString(preview.text) + {QStringLiteral("value"), displayed + (preview.truncated ? QStringLiteral(" … [truncated]") : QString{})}, + {QStringLiteral("engineeringEvidence"), engineering}, {QStringLiteral("authority"), QStringLiteral("SCL exact / MMS type checked")}, {QStringLiteral("reference"), full}}); if (preview.truncated) projection.textTruncated = true; From 7e026b5023cf32ec20f4a374c13c8cbe7cf84703 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 18:35:41 +0700 Subject: [PATCH 09/27] ui(p0): expand verified q/t Quality and TimeQuality beneath semantic report leaf --- apps/ied_simulator/qml/BrowserReportPane.qml | 68 ++++++++++++-------- 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/apps/ied_simulator/qml/BrowserReportPane.qml b/apps/ied_simulator/qml/BrowserReportPane.qml index d8b2c868b..fd153ed55 100644 --- a/apps/ied_simulator/qml/BrowserReportPane.qml +++ b/apps/ied_simulator/qml/BrowserReportPane.qml @@ -694,40 +694,52 @@ Rectangle { Repeater { model: root.selectedReportMemberIndex === receivedMember.index ? root.selectedReportMemberDetails() : [] - delegate: Rectangle { + delegate: ColumnLayout { + id: semanticLeafRow required property var modelData required property int index Layout.fillWidth: true - implicitHeight: 27 - color: index % 2 ? theme.surfaceSoft : theme.surface - RowLayout { - anchors.fill: parent - anchors.leftMargin: 18 - anchors.rightMargin: 9 - spacing: 10 - Label { - text: modelData.label - Layout.preferredWidth: 180 - elide: Text.ElideRight - font.pixelSize: 11 - color: theme.textSoft - } - Label { - text: modelData.value - Layout.fillWidth: true - elide: Text.ElideMiddle - font.pixelSize: 11 - color: theme.text - ToolTip.visible: detailMouse.containsMouse && truncated - ToolTip.text: text - MouseArea { - id: detailMouse - anchors.fill: parent - hoverEnabled: true - acceptedButtons: Qt.NoButton + spacing: 0 + Rectangle { + Layout.fillWidth: true + implicitHeight: 29 + color: semanticLeafRow.index % 2 + ? theme.surfaceSoft : theme.surface + RowLayout { + anchors.fill: parent + anchors.leftMargin: 18 + anchors.rightMargin: 9 + spacing: 10 + Label { + text: semanticLeafRow.modelData.label + Layout.preferredWidth: 180 + elide: Text.ElideRight + font.pixelSize: 11 + color: theme.textSoft + } + Label { + text: semanticLeafRow.modelData.value + Layout.fillWidth: true + elide: Text.ElideMiddle + font.pixelSize: 11 + color: theme.text + ToolTip.visible: detailMouse.containsMouse + ToolTip.text: semanticLeafRow.modelData.reference + || semanticLeafRow.modelData.value + MouseArea { + id: detailMouse + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + } } } } + BrowserEngineeringEvidence { + theme: root.theme + engineeringEvidence: semanticLeafRow.modelData.engineeringEvidence + || ({}) + } } } } From 818f732fbe0c94e28ce436423d2087733633131f Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 18:35:59 +0700 Subject: [PATCH 10/27] test(p0): verify typed quality/time evidence has native owner and inline GUI drill-down --- 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 c7ffb40f6..ef48083d1 100644 --- a/apps/ied_simulator/test_browser_convergence.py +++ b/apps/ied_simulator/test_browser_convergence.py @@ -304,6 +304,10 @@ ): if token not in report_controller: raise SystemExit("BROWSER_SEMANTIC_FAIL missing generation-owned runtime wiring=" + token) +if 'const auto engineering = decodeEngineeringEvidence(full, *leaf.value);' not in projection_header: + raise SystemExit("BROWSER_SEMANTIC_FAIL missing shared q/t native typed decoder") +if 'engineeringEvidence: semanticLeafRow.modelData.engineeringEvidence' not in report_inspector: + raise SystemExit("BROWSER_SEMANTIC_FAIL missing expandable child Quality/TimeQuality") if "SCL verified · MMS type checked" not in report_inspector: raise SystemExit("BROWSER_SEMANTIC_FAIL missing semantic/fallback provenance badge") From 7c156c3b5312a5d9df16cda9f3ed78c28bf12143 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 18:37:20 +0700 Subject: [PATCH 11/27] fix(p0): reject ambiguous SCL fields sharing compatible MMS wire type --- .../src/BrowserReportSemanticBinding.hpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/apps/ied_simulator/src/BrowserReportSemanticBinding.hpp b/apps/ied_simulator/src/BrowserReportSemanticBinding.hpp index 45b0cce37..bcb71c3bb 100644 --- a/apps/ied_simulator/src/BrowserReportSemanticBinding.hpp +++ b/apps/ied_simulator/src/BrowserReportSemanticBinding.hpp @@ -22,6 +22,16 @@ inline bool semanticInside(const std::string_view leaf, return leaf.size() > root.size() && leaf.substr(0,root.size()) == root && leaf[root.size()] == '.'; } +inline semantic_mms::MmsDataKind semanticExpectedKind(const std::string_view basic) { + using K = semantic_mms::MmsDataKind; + if (basic == "BOOLEAN") return K::boolean; + if (basic == "Quality" || basic == "QUALITY") return K::bit_string; + if (basic == "Timestamp" || basic == "TIMESTAMP") return K::utc_time; + if (basic == "Enum" || basic == "ENUM" || basic == "INT32" || + basic == "INT8" || basic == "INT16") return K::integer; + if (basic == "FLOAT32" || basic == "FLOAT64") return K::floating_point; + return K::unknown; +} inline bool semanticKindCompatible(const semantic_mms::MmsDataValue& value, const std::string_view basic) { using K = semantic_mms::MmsDataKind; @@ -112,7 +122,8 @@ inline std::optional> bindFlatSclReportStructure // valid and produce deceptively named engineering values. for (std::size_t i = 0; i < attributes.size(); ++i) for (std::size_t j = i + 1; j < attributes.size(); ++j) - if (attributes[i]->scl_basic_type == attributes[j]->scl_basic_type) + if (semanticExpectedKind(attributes[i]->scl_basic_type) == + semanticExpectedKind(attributes[j]->scl_basic_type)) return std::nullopt; std::vector result; result.reserve(children.size()); From 27b52235f66651985a3c8488be1bfe613705f6c4 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 18:37:34 +0700 Subject: [PATCH 12/27] fix(p0): bind missing optional DatSet from verified active RCB state --- apps/ied_simulator/src/MmsReportController.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/ied_simulator/src/MmsReportController.cpp b/apps/ied_simulator/src/MmsReportController.cpp index 576c0d36e..9f707d2a9 100644 --- a/apps/ied_simulator/src/MmsReportController.cpp +++ b/apps/ied_simulator/src/MmsReportController.cpp @@ -535,10 +535,19 @@ ReportUi buildSubscriptionUi( // Historical frames retain bounded scalar summaries and typed q/t // evidence; never allocate 32 x 16 child previews per old frame. const bool includeWireDetails = ui.reports.isEmpty(); + // DatSet is optional in OptFlds. Native report binding has already + // verified the subscribed RCB/DataSet directory; when absent use + // only that exact observed RCB state, never a guessed RptID. + const std::string_view verifiedDataSet = + !frame.header.data_set_reference.empty() + ? std::string_view{frame.header.data_set_reference} + : subscription.last_rcb_state + ? std::string_view{subscription.last_rcb_state->data_set_reference} + : std::string_view{}; const auto projected = arstack::iedsim::projectBrowserReportMembers( frame.values, typedEvidenceBudget, arstack::iedsim::kBrowserReportVisibleMemberLimit, - includeWireDetails, semanticModel, frame.header.data_set_reference); + includeWireDetails, semanticModel, verifiedDataSet); map.insert(QStringLiteral("values"), projected.values); map.insert(QStringLiteral("valueEvidence"), projected.evidence); map.insert(QStringLiteral("valueDetails"), projected.details); From c4cb126feffaba716b8ee5f8a46ca5963a0ca9b3 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 18:37:46 +0700 Subject: [PATCH 13/27] test(p0): enforce MMS-kind ambiguity rejection and verified optional DatSet fallback --- apps/ied_simulator/test_browser_convergence.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/ied_simulator/test_browser_convergence.py b/apps/ied_simulator/test_browser_convergence.py index ef48083d1..317cff22f 100644 --- a/apps/ied_simulator/test_browser_convergence.py +++ b/apps/ied_simulator/test_browser_convergence.py @@ -294,13 +294,15 @@ "attribute.functional_constraint != binding->functional_constraint", "attributes.size() != children.size()", "semanticKindCompatible(children[i], attributes[i]->scl_basic_type)", - "attributes[i]->scl_basic_type == attributes[j]->scl_basic_type", + "semanticExpectedKind(attributes[i]->scl_basic_type) ==", + "semanticExpectedKind(attributes[j]->scl_basic_type)", ): if token not in semantic: raise SystemExit("BROWSER_SEMANTIC_FAIL missing fail-closed binding=" + token) for token in ( "worker->semanticModel = contextModel;", - "semanticModel, frame.header.data_set_reference", + "semanticModel, verifiedDataSet", + "subscription.last_rcb_state->data_set_reference", ): if token not in report_controller: raise SystemExit("BROWSER_SEMANTIC_FAIL missing generation-owned runtime wiring=" + token) From 27fbd8527ec09c121304822ed0e6fbe20f13ae80 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 18:39:27 +0700 Subject: [PATCH 14/27] safety(p0): never use trusted SCL semantic model for a different configured MMS endpoint --- apps/ied_simulator/src/MmsReportController.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/ied_simulator/src/MmsReportController.cpp b/apps/ied_simulator/src/MmsReportController.cpp index 9f707d2a9..5a2798ade 100644 --- a/apps/ied_simulator/src/MmsReportController.cpp +++ b/apps/ied_simulator/src/MmsReportController.cpp @@ -789,8 +789,13 @@ bool MmsReportController::connectToIed() { if (engineeringContext_ && engineeringContext_->loaded() && !engineeringContext_->selectionRequired() && engineeringContext_->modelSnapshot() != nullptr) { - contextModel = std::make_shared( - *engineeringContext_->modelSnapshot()); + // The Browser may be pointed at a different endpoint from the + // opened SCL context. Never attach foreign schema to that MMS session. + const auto* candidate = engineeringContext_->modelSnapshot(); + if (candidate->endpoint.host == requestedHost.toStdString() && + candidate->endpoint.port == static_cast(requestedPort)) { + contextModel = std::make_shared(*candidate); + } } appendDiagnostic(QStringLiteral("Reports connect %1:%2 · generation %3") .arg(requestedHost).arg(requestedPort).arg(generation)); From e26a4595b10c18ef18176c17fe35795411e78c5d Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 18:39:39 +0700 Subject: [PATCH 15/27] test(p0): guard against applying opened SCL schema to other MMS endpoint --- apps/ied_simulator/test_browser_convergence.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/ied_simulator/test_browser_convergence.py b/apps/ied_simulator/test_browser_convergence.py index 317cff22f..c567bb6e0 100644 --- a/apps/ied_simulator/test_browser_convergence.py +++ b/apps/ied_simulator/test_browser_convergence.py @@ -301,6 +301,8 @@ raise SystemExit("BROWSER_SEMANTIC_FAIL missing fail-closed binding=" + token) for token in ( "worker->semanticModel = contextModel;", + "candidate->endpoint.host == requestedHost.toStdString()", + "candidate->endpoint.port == static_cast(requestedPort)", "semanticModel, verifiedDataSet", "subscription.last_rcb_state->data_set_reference", ): From 7158672a4d15ccd2c80dafd7f09d6dca7d914fb8 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 18:46:11 +0700 Subject: [PATCH 16/27] fix(p0): honor validated MMS DataRef aliases while enforcing exact canonical FCDA identity --- .../src/BrowserReportSemanticBinding.hpp | 204 +++++++++++++++++- 1 file changed, 199 insertions(+), 5 deletions(-) diff --git a/apps/ied_simulator/src/BrowserReportSemanticBinding.hpp b/apps/ied_simulator/src/BrowserReportSemanticBinding.hpp index bcb71c3bb..e342d2a97 100644 --- a/apps/ied_simulator/src/BrowserReportSemanticBinding.hpp +++ b/apps/ied_simulator/src/BrowserReportSemanticBinding.hpp @@ -75,12 +75,206 @@ inline std::optional> bindFlatSclReportStructure return std::nullopt; const auto& raw = member.data_reference; const auto& dir = member.member; - // Any supplied wire source must agree; do not let a directory alias - // override a contradictory explicit Report DataRef. - if (!raw.empty() && raw != binding->reference) return std::nullopt; + // Respect the aliases accepted by the native frame validator. DataRef + // may use IEC notation (LD/LN.DO) or MMS notation (LD/LN$FC$DO). + // It must still resolve to the exact SCL ordinal/reference/FC. + const auto matchesExactMember = [&](const std::string_view value) { + if (value.empty()) return false; + if (value == binding->reference || value == binding->mms_reference) + return true; + const auto slash = value.find('/'); + if (slash == std::string_view::npos || slash == 0U) return false; + const auto fcStart = value.find(' + if (dir && !dir->functional_constraint.empty() && + dir->functional_constraint != binding->functional_constraint) + return std::nullopt; + // Flat ST/CF/SG structures only. Nested, array, repeated or vendor + // layouts have no proven ordered schema here and remain positional. + const MmsLiveDataObject* object = nullptr; + for (const auto& ld : model.logical_devices) + for (const auto& ln : ld.logical_nodes) + for (const auto& candidate : ln.data_objects) + if (binding->reference == candidate.reference) { + if (object) return std::nullopt; + object = &candidate; + } + if (!object || object->confidence != MmsLiveModelConfidence::exact) + return std::nullopt; + std::vector attributes; + for (const auto& attribute : object->attributes) { + if (attribute.functional_constraint != binding->functional_constraint || + !semanticInside(attribute.object_reference, binding->reference)) + continue; + if (attribute.source != "TrustedSCL" || + attribute.type_confidence != MmsLiveModelConfidence::exact || + attribute.functional_constraint_confidence != MmsLiveModelConfidence::exact) + return std::nullopt; + // Only one direct segment is admissible; nested structures demand + // matching a hierarchical SCL schema, not guesswork. + const auto suffix = std::string_view{attribute.object_reference} + .substr(binding->reference.size() + 1); + if (suffix.empty() || suffix.find('.') != std::string_view::npos) + return std::nullopt; + attributes.push_back(&attribute); + } + const auto& children = member.value->children(); + if (attributes.empty() || attributes.size() != children.size() || + children.size() > 16U) return std::nullopt; + // Positional order is not independently proven by matching count alone. + // Reject repeated scalar types, where a vendor permutation would look + // valid and produce deceptively named engineering values. + for (std::size_t i = 0; i < attributes.size(); ++i) + for (std::size_t j = i + 1; j < attributes.size(); ++j) + if (semanticExpectedKind(attributes[i]->scl_basic_type) == + semanticExpectedKind(attributes[j]->scl_basic_type)) + return std::nullopt; + std::vector result; + result.reserve(children.size()); + for (std::size_t i = 0; i < children.size(); ++i) { + if (!semanticKindCompatible(children[i], attributes[i]->scl_basic_type)) + return std::nullopt; + if (std::any_of(result.begin(), result.end(), [&](const auto& leaf){ + return leaf.reference == attributes[i]->object_reference; })) + return std::nullopt; + result.push_back({attributes[i]->object_reference, &children[i]}); + } + return result; +} +} // namespace arstack::iedsim +, slash + 1U); + if (fcStart == std::string_view::npos) return false; + const auto pathStart = value.find(' + if (dir && !dir->functional_constraint.empty() && + dir->functional_constraint != binding->functional_constraint) + return std::nullopt; + // Flat ST/CF/SG structures only. Nested, array, repeated or vendor + // layouts have no proven ordered schema here and remain positional. + const MmsLiveDataObject* object = nullptr; + for (const auto& ld : model.logical_devices) + for (const auto& ln : ld.logical_nodes) + for (const auto& candidate : ln.data_objects) + if (binding->reference == candidate.reference) { + if (object) return std::nullopt; + object = &candidate; + } + if (!object || object->confidence != MmsLiveModelConfidence::exact) + return std::nullopt; + std::vector attributes; + for (const auto& attribute : object->attributes) { + if (attribute.functional_constraint != binding->functional_constraint || + !semanticInside(attribute.object_reference, binding->reference)) + continue; + if (attribute.source != "TrustedSCL" || + attribute.type_confidence != MmsLiveModelConfidence::exact || + attribute.functional_constraint_confidence != MmsLiveModelConfidence::exact) + return std::nullopt; + // Only one direct segment is admissible; nested structures demand + // matching a hierarchical SCL schema, not guesswork. + const auto suffix = std::string_view{attribute.object_reference} + .substr(binding->reference.size() + 1); + if (suffix.empty() || suffix.find('.') != std::string_view::npos) + return std::nullopt; + attributes.push_back(&attribute); + } + const auto& children = member.value->children(); + if (attributes.empty() || attributes.size() != children.size() || + children.size() > 16U) return std::nullopt; + // Positional order is not independently proven by matching count alone. + // Reject repeated scalar types, where a vendor permutation would look + // valid and produce deceptively named engineering values. + for (std::size_t i = 0; i < attributes.size(); ++i) + for (std::size_t j = i + 1; j < attributes.size(); ++j) + if (semanticExpectedKind(attributes[i]->scl_basic_type) == + semanticExpectedKind(attributes[j]->scl_basic_type)) + return std::nullopt; + std::vector result; + result.reserve(children.size()); + for (std::size_t i = 0; i < children.size(); ++i) { + if (!semanticKindCompatible(children[i], attributes[i]->scl_basic_type)) + return std::nullopt; + if (std::any_of(result.begin(), result.end(), [&](const auto& leaf){ + return leaf.reference == attributes[i]->object_reference; })) + return std::nullopt; + result.push_back({attributes[i]->object_reference, &children[i]}); + } + return result; +} +} // namespace arstack::iedsim +, fcStart + 1U); + if (pathStart == std::string_view::npos || + value.substr(fcStart + 1U, pathStart - fcStart - 1U) != + binding->functional_constraint) + return false; + std::string canonical{value.substr(0, fcStart)}; + canonical.push_back('.'); + for (std::size_t i = pathStart + 1U; i < value.size(); ++i) + canonical.push_back(value[i] == ' + if (dir && !dir->functional_constraint.empty() && + dir->functional_constraint != binding->functional_constraint) + return std::nullopt; + // Flat ST/CF/SG structures only. Nested, array, repeated or vendor + // layouts have no proven ordered schema here and remain positional. + const MmsLiveDataObject* object = nullptr; + for (const auto& ld : model.logical_devices) + for (const auto& ln : ld.logical_nodes) + for (const auto& candidate : ln.data_objects) + if (binding->reference == candidate.reference) { + if (object) return std::nullopt; + object = &candidate; + } + if (!object || object->confidence != MmsLiveModelConfidence::exact) + return std::nullopt; + std::vector attributes; + for (const auto& attribute : object->attributes) { + if (attribute.functional_constraint != binding->functional_constraint || + !semanticInside(attribute.object_reference, binding->reference)) + continue; + if (attribute.source != "TrustedSCL" || + attribute.type_confidence != MmsLiveModelConfidence::exact || + attribute.functional_constraint_confidence != MmsLiveModelConfidence::exact) + return std::nullopt; + // Only one direct segment is admissible; nested structures demand + // matching a hierarchical SCL schema, not guesswork. + const auto suffix = std::string_view{attribute.object_reference} + .substr(binding->reference.size() + 1); + if (suffix.empty() || suffix.find('.') != std::string_view::npos) + return std::nullopt; + attributes.push_back(&attribute); + } + const auto& children = member.value->children(); + if (attributes.empty() || attributes.size() != children.size() || + children.size() > 16U) return std::nullopt; + // Positional order is not independently proven by matching count alone. + // Reject repeated scalar types, where a vendor permutation would look + // valid and produce deceptively named engineering values. + for (std::size_t i = 0; i < attributes.size(); ++i) + for (std::size_t j = i + 1; j < attributes.size(); ++j) + if (semanticExpectedKind(attributes[i]->scl_basic_type) == + semanticExpectedKind(attributes[j]->scl_basic_type)) + return std::nullopt; + std::vector result; + result.reserve(children.size()); + for (std::size_t i = 0; i < children.size(); ++i) { + if (!semanticKindCompatible(children[i], attributes[i]->scl_basic_type)) + return std::nullopt; + if (std::any_of(result.begin(), result.end(), [&](const auto& leaf){ + return leaf.reference == attributes[i]->object_reference; })) + return std::nullopt; + result.push_back({attributes[i]->object_reference, &children[i]}); + } + return result; +} +} // namespace arstack::iedsim + ? '.' : value[i]); + return canonical == binding->reference; + }; + if (!raw.empty() && !matchesExactMember(raw)) return std::nullopt; if (dir && !dir->user_reference.empty() && - dir->user_reference != binding->reference) return std::nullopt; - if (raw.empty() && (!dir || dir->user_reference.empty())) + !matchesExactMember(dir->user_reference)) return std::nullopt; + if (dir && !dir->mms_reference.empty() && + !matchesExactMember(dir->mms_reference)) return std::nullopt; + if (raw.empty() && (!dir || (dir->user_reference.empty() && + dir->mms_reference.empty()))) return std::nullopt; if (dir && !dir->functional_constraint.empty() && dir->functional_constraint != binding->functional_constraint) From 06d1b79a2191ec0706808630c7ca461cf8a5d845 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 18:46:28 +0700 Subject: [PATCH 17/27] test(p0): verify two local q/t evidence inspectors and validated MMS alias mapping --- apps/ied_simulator/test_browser_convergence.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/ied_simulator/test_browser_convergence.py b/apps/ied_simulator/test_browser_convergence.py index c567bb6e0..ca29fddef 100644 --- a/apps/ied_simulator/test_browser_convergence.py +++ b/apps/ied_simulator/test_browser_convergence.py @@ -281,8 +281,12 @@ ): if token not in report_inspector: raise SystemExit("BROWSER_REPORT_INLINE_TREE_FAIL compact selection=" + token) -if report_inspector.count("BrowserEngineeringEvidence {") != 1: - raise SystemExit("BROWSER_REPORT_INLINE_TREE_FAIL duplicate or detached q/t inspector") +# One member-level inspector and one nested semantic-leaf inspector. Both +# must live inside the selected member's lazy Loader, not after the DataSet. +if (member_tree.count("BrowserEngineeringEvidence {") != 2 or + report_inspector.count("BrowserEngineeringEvidence {") != 2 or + "engineeringEvidence: semanticLeafRow.modelData.engineeringEvidence" not in member_tree): + raise SystemExit("BROWSER_REPORT_INLINE_TREE_FAIL incorrect inline q/t inspectors") # P0 semantic projection borrows ARIEC61850 exact DataSet/FCDA/type gates. semantic = (workspace_path.parent.parent / "src" / "BrowserReportSemanticBinding.hpp").read_text(encoding="utf-8") @@ -290,7 +294,9 @@ "model.identity.confidence != MmsLiveModelConfidence::exact", 'model.source != "TrustedSclInitialSnapshot"', "fcda.index != member.data_set_index", - "dir->user_reference != binding->reference", + "matchesExactMember(dir->user_reference)", + "matchesExactMember(dir->mms_reference)", + "canonical == binding->reference", "attribute.functional_constraint != binding->functional_constraint", "attributes.size() != children.size()", "semanticKindCompatible(children[i], attributes[i]->scl_basic_type)", From 9b75c1b8aaab64fa20c172ed14f622c14d442275 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 18:48:15 +0700 Subject: [PATCH 18/27] safety(p0): validate exact MMS FCDA aliases and reject malformed 13-bit Quality --- .../src/BrowserReportSemanticBinding.hpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/ied_simulator/src/BrowserReportSemanticBinding.hpp b/apps/ied_simulator/src/BrowserReportSemanticBinding.hpp index e342d2a97..b7d5321ae 100644 --- a/apps/ied_simulator/src/BrowserReportSemanticBinding.hpp +++ b/apps/ied_simulator/src/BrowserReportSemanticBinding.hpp @@ -36,7 +36,14 @@ inline bool semanticKindCompatible(const semantic_mms::MmsDataValue& value, const std::string_view basic) { using K = semantic_mms::MmsDataKind; if (basic == "BOOLEAN") return value.kind() == K::boolean; - if (basic == "Quality" || basic == "QUALITY") return value.kind() == K::bit_string; + if (basic == "Quality" || basic == "QUALITY") { + if (value.kind() != K::bit_string) return false; + const auto& bits = value.raw_value(); + // IEC 61850 q has precisely 13 defined bits; a short/overlong or + // nonzero padding value cannot authorize a semantic quality label. + return bits.size() == 3U && bits[0] == 3U && + (bits[2] & 0x07U) == 0U; + } if (basic == "Timestamp" || basic == "TIMESTAMP") return value.kind() == K::utc_time; if (basic == "Enum" || basic == "ENUM" || basic == "INT32" || basic == "INT8" || basic == "INT16") return value.kind() == K::integer; @@ -80,8 +87,9 @@ inline std::optional> bindFlatSclReportStructure // It must still resolve to the exact SCL ordinal/reference/FC. const auto matchesExactMember = [&](const std::string_view value) { if (value.empty()) return false; - if (value == binding->reference || value == binding->mms_reference) - return true; + if (value == binding->reference) return true; + // Even the SCL's MMS alias must reduce to this exact FCDA identity. + // A stale/incorrect alias must never certify a foreign member. const auto slash = value.find('/'); if (slash == std::string_view::npos || slash == 0U) return false; const auto fcStart = value.find(' From d15401c7fb02ab4525cfbc7de67f3d6722120125 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 18:49:07 +0700 Subject: [PATCH 19/27] test(p0): cover exact MMS DataRef alias, wrong FC/leaf and malformed Quality bits --- .../src/ied_report_workspace_qa.cpp | 21 ++++++++++++++++--- 1 file changed, 18 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 675532dae..e4d57fa68 100644 --- a/apps/ied_simulator/src/ied_report_workspace_qa.cpp +++ b/apps/ied_simulator/src/ied_report_workspace_qa.cpp @@ -212,12 +212,27 @@ bool verifySemanticReportBinding() { value.data_reference = binding.reference; value.value = mms::MmsDataValue::structure({ mms::MmsDataValue::integer(2), - mms::MmsDataValue::bit_string(0U, quality)}); + mms::MmsDataValue::bit_string(3U, quality)}); auto check = [&](const bool expected) { const auto result = bindFlatSclReportStructure(model, dataset.reference, value); return result.has_value() == expected; }; if (!check(true)) return false; + value.data_reference = "IEDLD/XCBR1$ST$Pos"; + if (!check(true)) return false; // valid native MMS DataRef alias + value.data_reference = "IEDLD/XCBR1$MX$Pos"; + if (!check(false)) return false; // conflicting FC + value.data_reference = "IEDLD/XCBR1$ST$Other"; + if (!check(false)) return false; // conflicting member + value.data_reference = binding.reference; + value.value = mms::MmsDataValue::structure({ + mms::MmsDataValue::integer(2), + mms::MmsDataValue::bit_string(0U, quality)}); + if (!check(false)) return false; // invalid 16-bit q, not 13-bit + value.value = mms::MmsDataValue::structure({ + mms::MmsDataValue::integer(2), + mms::MmsDataValue::bit_string(3U, quality)}); + if (!check(true)) return false; value.data_reference = "OtherIED/XCBR1.Pos"; if (!check(false)) return false; value.data_reference = binding.reference; @@ -233,7 +248,7 @@ bool verifySemanticReportBinding() { mms::MmsDataValue::integer(2), mms::MmsDataValue::integer(3)}); if (!check(false)) return false; value.value = mms::MmsDataValue::structure({ - mms::MmsDataValue::integer(2), mms::MmsDataValue::bit_string(0U, quality)}); + mms::MmsDataValue::integer(2), mms::MmsDataValue::bit_string(3U, quality)}); model.logical_devices[0].logical_nodes[0].data_objects[0].attributes[1].scl_basic_type = "Enum"; if (!check(false)) return false; return true; @@ -248,7 +263,7 @@ int main(int argc, char* argv[]) { qCritical() << "REPORTS_SEMANTIC_BINDING_FAIL"; return 42; } - qInfo() << "REPORTS_SEMANTIC_BINDING_PASS exact=1 mismatch=6"; + qInfo() << "REPORTS_SEMANTIC_BINDING_PASS canonical=pass mms_alias=pass quality=pass negative=pass"; if (argc != 2) { qCritical() << "usage: ied_report_workspace_qa "; return 2; From 825983582e98146140503705618ad3cad9223b91 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 18:49:21 +0700 Subject: [PATCH 20/27] test(p0): guard malformed IEC Quality rejection alongside alias normalization --- apps/ied_simulator/test_browser_convergence.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/ied_simulator/test_browser_convergence.py b/apps/ied_simulator/test_browser_convergence.py index ca29fddef..d4770b12e 100644 --- a/apps/ied_simulator/test_browser_convergence.py +++ b/apps/ied_simulator/test_browser_convergence.py @@ -297,6 +297,7 @@ "matchesExactMember(dir->user_reference)", "matchesExactMember(dir->mms_reference)", "canonical == binding->reference", + "bits.size() == 3U && bits[0] == 3U", "attribute.functional_constraint != binding->functional_constraint", "attributes.size() != children.size()", "semanticKindCompatible(children[i], attributes[i]->scl_basic_type)", From 7f7d28bb84d75180524fd3498088a4cb3c251f8b Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 18:53:39 +0700 Subject: [PATCH 21/27] fix(p0): restore clean binder and apply exact alias, quality and Open SCL guards --- .../src/BrowserReportSemanticBinding.hpp | 197 ++---------------- 1 file changed, 14 insertions(+), 183 deletions(-) diff --git a/apps/ied_simulator/src/BrowserReportSemanticBinding.hpp b/apps/ied_simulator/src/BrowserReportSemanticBinding.hpp index b7d5321ae..942f38603 100644 --- a/apps/ied_simulator/src/BrowserReportSemanticBinding.hpp +++ b/apps/ied_simulator/src/BrowserReportSemanticBinding.hpp @@ -39,8 +39,6 @@ inline bool semanticKindCompatible(const semantic_mms::MmsDataValue& value, if (basic == "Quality" || basic == "QUALITY") { if (value.kind() != K::bit_string) return false; const auto& bits = value.raw_value(); - // IEC 61850 q has precisely 13 defined bits; a short/overlong or - // nonzero padding value cannot authorize a semantic quality label. return bits.size() == 3U && bits[0] == 3U && (bits[2] & 0x07U) == 0U; } @@ -61,8 +59,13 @@ inline std::optional> bindFlatSclReportStructure return std::nullopt; // SCL identity/model must be trusted and the FCDA directory must be // uniquely bound to this DataSet and exact ordinal. Never infer by RptID. + // Canonical Open SCL and SCL-assisted contexts share the same existing + // MMS report runtime; the caller binds the selected model to this endpoint + // and the runtime has validated the frame against the live RCB/DataSet. + const bool openedScl = model.source == "OpenedSCL"; + const bool trustedSnapshot = model.source == "TrustedSclInitialSnapshot"; if (model.identity.confidence != MmsLiveModelConfidence::exact || - model.source != "TrustedSclInitialSnapshot" || dataSet.empty()) + (!openedScl && !trustedSnapshot) || dataSet.empty()) return std::nullopt; const MmsLiveDataSet* chosen = nullptr; for (const auto& ds : model.data_sets) { @@ -82,133 +85,17 @@ inline std::optional> bindFlatSclReportStructure return std::nullopt; const auto& raw = member.data_reference; const auto& dir = member.member; - // Respect the aliases accepted by the native frame validator. DataRef - // may use IEC notation (LD/LN.DO) or MMS notation (LD/LN$FC$DO). - // It must still resolve to the exact SCL ordinal/reference/FC. + // The native frame validator accepts IEC and MMS DataRef notation. + // Preserve those aliases only when they resolve to the exact same + // canonical object reference and functional constraint. const auto matchesExactMember = [&](const std::string_view value) { if (value.empty()) return false; if (value == binding->reference) return true; - // Even the SCL's MMS alias must reduce to this exact FCDA identity. - // A stale/incorrect alias must never certify a foreign member. const auto slash = value.find('/'); if (slash == std::string_view::npos || slash == 0U) return false; - const auto fcStart = value.find(' - if (dir && !dir->functional_constraint.empty() && - dir->functional_constraint != binding->functional_constraint) - return std::nullopt; - // Flat ST/CF/SG structures only. Nested, array, repeated or vendor - // layouts have no proven ordered schema here and remain positional. - const MmsLiveDataObject* object = nullptr; - for (const auto& ld : model.logical_devices) - for (const auto& ln : ld.logical_nodes) - for (const auto& candidate : ln.data_objects) - if (binding->reference == candidate.reference) { - if (object) return std::nullopt; - object = &candidate; - } - if (!object || object->confidence != MmsLiveModelConfidence::exact) - return std::nullopt; - std::vector attributes; - for (const auto& attribute : object->attributes) { - if (attribute.functional_constraint != binding->functional_constraint || - !semanticInside(attribute.object_reference, binding->reference)) - continue; - if (attribute.source != "TrustedSCL" || - attribute.type_confidence != MmsLiveModelConfidence::exact || - attribute.functional_constraint_confidence != MmsLiveModelConfidence::exact) - return std::nullopt; - // Only one direct segment is admissible; nested structures demand - // matching a hierarchical SCL schema, not guesswork. - const auto suffix = std::string_view{attribute.object_reference} - .substr(binding->reference.size() + 1); - if (suffix.empty() || suffix.find('.') != std::string_view::npos) - return std::nullopt; - attributes.push_back(&attribute); - } - const auto& children = member.value->children(); - if (attributes.empty() || attributes.size() != children.size() || - children.size() > 16U) return std::nullopt; - // Positional order is not independently proven by matching count alone. - // Reject repeated scalar types, where a vendor permutation would look - // valid and produce deceptively named engineering values. - for (std::size_t i = 0; i < attributes.size(); ++i) - for (std::size_t j = i + 1; j < attributes.size(); ++j) - if (semanticExpectedKind(attributes[i]->scl_basic_type) == - semanticExpectedKind(attributes[j]->scl_basic_type)) - return std::nullopt; - std::vector result; - result.reserve(children.size()); - for (std::size_t i = 0; i < children.size(); ++i) { - if (!semanticKindCompatible(children[i], attributes[i]->scl_basic_type)) - return std::nullopt; - if (std::any_of(result.begin(), result.end(), [&](const auto& leaf){ - return leaf.reference == attributes[i]->object_reference; })) - return std::nullopt; - result.push_back({attributes[i]->object_reference, &children[i]}); - } - return result; -} -} // namespace arstack::iedsim -, slash + 1U); + const auto fcStart = value.find('$', slash + 1U); if (fcStart == std::string_view::npos) return false; - const auto pathStart = value.find(' - if (dir && !dir->functional_constraint.empty() && - dir->functional_constraint != binding->functional_constraint) - return std::nullopt; - // Flat ST/CF/SG structures only. Nested, array, repeated or vendor - // layouts have no proven ordered schema here and remain positional. - const MmsLiveDataObject* object = nullptr; - for (const auto& ld : model.logical_devices) - for (const auto& ln : ld.logical_nodes) - for (const auto& candidate : ln.data_objects) - if (binding->reference == candidate.reference) { - if (object) return std::nullopt; - object = &candidate; - } - if (!object || object->confidence != MmsLiveModelConfidence::exact) - return std::nullopt; - std::vector attributes; - for (const auto& attribute : object->attributes) { - if (attribute.functional_constraint != binding->functional_constraint || - !semanticInside(attribute.object_reference, binding->reference)) - continue; - if (attribute.source != "TrustedSCL" || - attribute.type_confidence != MmsLiveModelConfidence::exact || - attribute.functional_constraint_confidence != MmsLiveModelConfidence::exact) - return std::nullopt; - // Only one direct segment is admissible; nested structures demand - // matching a hierarchical SCL schema, not guesswork. - const auto suffix = std::string_view{attribute.object_reference} - .substr(binding->reference.size() + 1); - if (suffix.empty() || suffix.find('.') != std::string_view::npos) - return std::nullopt; - attributes.push_back(&attribute); - } - const auto& children = member.value->children(); - if (attributes.empty() || attributes.size() != children.size() || - children.size() > 16U) return std::nullopt; - // Positional order is not independently proven by matching count alone. - // Reject repeated scalar types, where a vendor permutation would look - // valid and produce deceptively named engineering values. - for (std::size_t i = 0; i < attributes.size(); ++i) - for (std::size_t j = i + 1; j < attributes.size(); ++j) - if (semanticExpectedKind(attributes[i]->scl_basic_type) == - semanticExpectedKind(attributes[j]->scl_basic_type)) - return std::nullopt; - std::vector result; - result.reserve(children.size()); - for (std::size_t i = 0; i < children.size(); ++i) { - if (!semanticKindCompatible(children[i], attributes[i]->scl_basic_type)) - return std::nullopt; - if (std::any_of(result.begin(), result.end(), [&](const auto& leaf){ - return leaf.reference == attributes[i]->object_reference; })) - return std::nullopt; - result.push_back({attributes[i]->object_reference, &children[i]}); - } - return result; -} -} // namespace arstack::iedsim -, fcStart + 1U); + const auto pathStart = value.find('$', fcStart + 1U); if (pathStart == std::string_view::npos || value.substr(fcStart + 1U, pathStart - fcStart - 1U) != binding->functional_constraint) @@ -216,64 +103,7 @@ inline std::optional> bindFlatSclReportStructure std::string canonical{value.substr(0, fcStart)}; canonical.push_back('.'); for (std::size_t i = pathStart + 1U; i < value.size(); ++i) - canonical.push_back(value[i] == ' - if (dir && !dir->functional_constraint.empty() && - dir->functional_constraint != binding->functional_constraint) - return std::nullopt; - // Flat ST/CF/SG structures only. Nested, array, repeated or vendor - // layouts have no proven ordered schema here and remain positional. - const MmsLiveDataObject* object = nullptr; - for (const auto& ld : model.logical_devices) - for (const auto& ln : ld.logical_nodes) - for (const auto& candidate : ln.data_objects) - if (binding->reference == candidate.reference) { - if (object) return std::nullopt; - object = &candidate; - } - if (!object || object->confidence != MmsLiveModelConfidence::exact) - return std::nullopt; - std::vector attributes; - for (const auto& attribute : object->attributes) { - if (attribute.functional_constraint != binding->functional_constraint || - !semanticInside(attribute.object_reference, binding->reference)) - continue; - if (attribute.source != "TrustedSCL" || - attribute.type_confidence != MmsLiveModelConfidence::exact || - attribute.functional_constraint_confidence != MmsLiveModelConfidence::exact) - return std::nullopt; - // Only one direct segment is admissible; nested structures demand - // matching a hierarchical SCL schema, not guesswork. - const auto suffix = std::string_view{attribute.object_reference} - .substr(binding->reference.size() + 1); - if (suffix.empty() || suffix.find('.') != std::string_view::npos) - return std::nullopt; - attributes.push_back(&attribute); - } - const auto& children = member.value->children(); - if (attributes.empty() || attributes.size() != children.size() || - children.size() > 16U) return std::nullopt; - // Positional order is not independently proven by matching count alone. - // Reject repeated scalar types, where a vendor permutation would look - // valid and produce deceptively named engineering values. - for (std::size_t i = 0; i < attributes.size(); ++i) - for (std::size_t j = i + 1; j < attributes.size(); ++j) - if (semanticExpectedKind(attributes[i]->scl_basic_type) == - semanticExpectedKind(attributes[j]->scl_basic_type)) - return std::nullopt; - std::vector result; - result.reserve(children.size()); - for (std::size_t i = 0; i < children.size(); ++i) { - if (!semanticKindCompatible(children[i], attributes[i]->scl_basic_type)) - return std::nullopt; - if (std::any_of(result.begin(), result.end(), [&](const auto& leaf){ - return leaf.reference == attributes[i]->object_reference; })) - return std::nullopt; - result.push_back({attributes[i]->object_reference, &children[i]}); - } - return result; -} -} // namespace arstack::iedsim - ? '.' : value[i]); + canonical.push_back(value[i] == '$' ? '.' : value[i]); return canonical == binding->reference; }; if (!raw.empty() && !matchesExactMember(raw)) return std::nullopt; @@ -304,7 +134,8 @@ inline std::optional> bindFlatSclReportStructure if (attribute.functional_constraint != binding->functional_constraint || !semanticInside(attribute.object_reference, binding->reference)) continue; - if (attribute.source != "TrustedSCL" || + if (attribute.source != + (openedScl ? "OpenedSCL" : "TrustedSCL") || attribute.type_confidence != MmsLiveModelConfidence::exact || attribute.functional_constraint_confidence != MmsLiveModelConfidence::exact) return std::nullopt; From 5d45f2284691e1b0eda413a7f69d9e56a48f1cde Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 18:54:19 +0700 Subject: [PATCH 22/27] test(p0): prove native opened SCL model and reject unverified model source --- apps/ied_simulator/src/ied_report_workspace_qa.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/ied_simulator/src/ied_report_workspace_qa.cpp b/apps/ied_simulator/src/ied_report_workspace_qa.cpp index e4d57fa68..ed203242c 100644 --- a/apps/ied_simulator/src/ied_report_workspace_qa.cpp +++ b/apps/ied_simulator/src/ied_report_workspace_qa.cpp @@ -218,6 +218,16 @@ bool verifySemanticReportBinding() { return result.has_value() == expected; }; if (!check(true)) return false; + // The real Browser Open SCL owner uses a distinct source marker from + // the SCL-assisted initial snapshot. Both must work with exact evidence. + model.source = "OpenedSCL"; + for (auto& attribute : model.logical_devices[0].logical_nodes[0] + .data_objects[0].attributes) + attribute.source = "OpenedSCL"; + if (!check(true)) return false; + model.source = "UnverifiedSCL"; + if (!check(false)) return false; + model.source = "OpenedSCL"; value.data_reference = "IEDLD/XCBR1$ST$Pos"; if (!check(true)) return false; // valid native MMS DataRef alias value.data_reference = "IEDLD/XCBR1$MX$Pos"; From c915b1c084f1b57c6439640f9b3f708e8ab563d5 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 18:54:23 +0700 Subject: [PATCH 23/27] test(p0): pin Open SCL semantic source handling in convergence guard --- apps/ied_simulator/test_browser_convergence.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/ied_simulator/test_browser_convergence.py b/apps/ied_simulator/test_browser_convergence.py index d4770b12e..9e19085e9 100644 --- a/apps/ied_simulator/test_browser_convergence.py +++ b/apps/ied_simulator/test_browser_convergence.py @@ -292,7 +292,9 @@ semantic = (workspace_path.parent.parent / "src" / "BrowserReportSemanticBinding.hpp").read_text(encoding="utf-8") for token in ( "model.identity.confidence != MmsLiveModelConfidence::exact", - 'model.source != "TrustedSclInitialSnapshot"', + 'model.source == "OpenedSCL"', + 'model.source == "TrustedSclInitialSnapshot"', + '(openedScl ? "OpenedSCL" : "TrustedSCL")', "fcda.index != member.data_set_index", "matchesExactMember(dir->user_reference)", "matchesExactMember(dir->mms_reference)", From 6a76c089c4416b5f79ca4f983645432967ff8ebe Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 18:55:19 +0700 Subject: [PATCH 24/27] fix(p0): compare native MMS DataSet ObjectName identity across SCL notation --- .../src/BrowserReportSemanticBinding.hpp | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/apps/ied_simulator/src/BrowserReportSemanticBinding.hpp b/apps/ied_simulator/src/BrowserReportSemanticBinding.hpp index 942f38603..d41a39c97 100644 --- a/apps/ied_simulator/src/BrowserReportSemanticBinding.hpp +++ b/apps/ied_simulator/src/BrowserReportSemanticBinding.hpp @@ -68,10 +68,20 @@ inline std::optional> bindFlatSclReportStructure (!openedScl && !trustedSnapshot) || dataSet.empty()) return std::nullopt; const MmsLiveDataSet* chosen = nullptr; - for (const auto& ds : model.data_sets) { - if (ds.reference != dataSet) continue; - if (chosen) return std::nullopt; - chosen = &ds; + // Reuse the ObjectName normalization accepted by the native frame + // validator: dot and dollar forms must resolve to one DataSet identity. + try { + const auto requested = + MmsDataSetDirectoryCodec::parse_data_set_reference(std::string{dataSet}); + for (const auto& ds : model.data_sets) { + if (MmsDataSetDirectoryCodec::parse_data_set_reference(ds.reference) + != requested) + continue; + if (chosen) return std::nullopt; + chosen = &ds; + } + } catch (const std::exception&) { + return std::nullopt; } if (!chosen) return std::nullopt; const MmsLiveDataSetMember* binding = nullptr; From fc3ddf3e68132fd027c9ca404ccbb10d4b08334a Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 18:55:29 +0700 Subject: [PATCH 25/27] perf(p0): cap verified semantic detail to first 32 report members --- apps/ied_simulator/src/BrowserReportMemberProjection.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/ied_simulator/src/BrowserReportMemberProjection.hpp b/apps/ied_simulator/src/BrowserReportMemberProjection.hpp index 1d817b83e..9a04cb066 100644 --- a/apps/ied_simulator/src/BrowserReportMemberProjection.hpp +++ b/apps/ied_simulator/src/BrowserReportMemberProjection.hpp @@ -116,7 +116,8 @@ inline constexpr int kBrowserReportDetailChildren = 16; .arg(includeWireDetails && ordinal < kBrowserReportDetailMembers ? QStringLiteral(" — select for details") : QString{}); - const auto verified = includeWireDetails && trustedModel + const auto verified = includeWireDetails && trustedModel && + ordinal < kBrowserReportDetailMembers ? bindFlatSclReportStructure(*trustedModel, dataSetReference, member) : std::nullopt; if (verified) { From 307e252b43c0f997d309fe0920b66644f6d0efef Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 18:55:48 +0700 Subject: [PATCH 26/27] test(p0): prove native MMS DataSet-name alias in semantic binding --- 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 ed203242c..d848bb935 100644 --- a/apps/ied_simulator/src/ied_report_workspace_qa.cpp +++ b/apps/ied_simulator/src/ied_report_workspace_qa.cpp @@ -218,6 +218,8 @@ bool verifySemanticReportBinding() { return result.has_value() == expected; }; if (!check(true)) return false; + if (!bindFlatSclReportStructure(model, "IEDLD/LLN0$ds", value)) + return false; // Same DataSet via MMS native ObjectName alias // The real Browser Open SCL owner uses a distinct source marker from // the SCL-assisted initial snapshot. Both must work with exact evidence. model.source = "OpenedSCL"; From b55452b85f29b59f0dc88e3be12aa5b8b9f264f1 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 18:55:52 +0700 Subject: [PATCH 27/27] test(p0): guard DataSet normalization and bounded verified detail budget --- apps/ied_simulator/test_browser_convergence.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/ied_simulator/test_browser_convergence.py b/apps/ied_simulator/test_browser_convergence.py index 9e19085e9..926590c97 100644 --- a/apps/ied_simulator/test_browser_convergence.py +++ b/apps/ied_simulator/test_browser_convergence.py @@ -305,6 +305,7 @@ "semanticKindCompatible(children[i], attributes[i]->scl_basic_type)", "semanticExpectedKind(attributes[i]->scl_basic_type) ==", "semanticExpectedKind(attributes[j]->scl_basic_type)", + "MmsDataSetDirectoryCodec::parse_data_set_reference", ): if token not in semantic: raise SystemExit("BROWSER_SEMANTIC_FAIL missing fail-closed binding=" + token) @@ -317,6 +318,8 @@ ): if token not in report_controller: raise SystemExit("BROWSER_SEMANTIC_FAIL missing generation-owned runtime wiring=" + token) +if 'ordinal < kBrowserReportDetailMembers' not in projection_header: + raise SystemExit("BROWSER_SEMANTIC_FAIL uncapped verified details") if 'const auto engineering = decodeEngineeringEvidence(full, *leaf.value);' not in projection_header: raise SystemExit("BROWSER_SEMANTIC_FAIL missing shared q/t native typed decoder") if 'engineeringEvidence: semanticLeafRow.modelData.engineeringEvidence' not in report_inspector: