checkContinuity: catch cross-folio report-link colour/style mismatches

New report_link_mismatch finding (severity "warning", not "error" the
way potential_mismatch is): a next_report/previous_report folio-jump
pair whose conductors disagree on colour, style, num, or any of the
other checked properties. Unlike potential_mismatch, this one is not
proof of external tampering -- LinkElementCommand::isLinkable() only
ever checks type and freedom (see its own doc comment), never conductor
properties, so nothing in QElectroTech copies one side's colour onto
the other when a report link is made or keeps them in sync afterwards.
This is a real, unenforced gap reachable through completely ordinary
use, not a defect a script or the GUI could introduce.

Reproduces qelectrotech/qelectrotech-source-mirror#974 exactly:
downloaded the reporter's actual project, traced the mismatched wire to
a "Folio suivant"/"Folio precedent" link pair, and confirmed via query
that the two sides read color="#0000ff" and color="#55aa00" while the
link's other four conductors (0V/Low/High/Ground) matched -- ruling out
a rendering artifact. Verified fresh with a synthetic reproduction
(tests in misc/qet-mcp) using the shipped 02going_arrow.elmt/
01coming_arrow.elmt pair, giving exactly one finding, not one per
folio-link conductor.

Also fixes a real gap in the existing potential_mismatch check while
here: checked_properties was missing "color" and "style" entirely,
checking only "conductor_color" (ConductorProperties::m_wire_color, a
separate free-text documentation field, typically empty) -- meaning
the same-folio version of this exact bug class would have gone
undetected too.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
ispyisail
2026-09-22 22:36:45 +12:00
parent 3b05f5cf13
commit 68503e2a7d
+78 -3
View File
@@ -3912,10 +3912,18 @@ QVariantList QetScriptApi::checkContinuity(int folioIndex)
}
}
// "color"/"style" are the rendered pen -- what a person actually sees
// as "this wire is blue" -- distinct from "conductor_color"
// (ConductorProperties::m_wire_color), a separate free-text
// documentation field that is typically empty and says nothing about
// how the wire is drawn. A mismatch check that only covered the
// documentation field would miss exactly the visible kind of
// inconsistency a person would report (issue #974: the two halves of
// one folio-link conductor drawn in different colours).
static const QStringList checked_properties = {
QStringLiteral("num"), QStringLiteral("conductor_color"),
QStringLiteral("conductor_section"), QStringLiteral("function"),
QStringLiteral("bus"), QStringLiteral("cable")};
QStringLiteral("num"), QStringLiteral("color"), QStringLiteral("style"),
QStringLiteral("conductor_color"), QStringLiteral("conductor_section"),
QStringLiteral("function"), QStringLiteral("bus"), QStringLiteral("cable")};
QSet<Conductor *> visited;
for (int f = 0; f < diagrams.count(); ++f) {
@@ -3954,6 +3962,73 @@ QVariantList QetScriptApi::checkContinuity(int folioIndex)
}
}
// report_link_mismatch (warning, not error): a next_report/
// previous_report pair -- QElectroTech's folio-jump-arrow links,
// e.g. "Folio suivant"/"Folio précédent" -- is meant to represent one
// wire continuing across a folio boundary, but LinkElementCommand::
// isLinkable() only ever checks type and freedom (see its own doc
// comment), never conductor properties. Nothing in QElectroTech
// copies one side's colour/style/num onto the other when the link is
// made, or keeps them in sync afterwards, so this is a real,
// unenforced gap rather than something a script or the GUI could
// have broken -- hence "warning", not "error" the way
// potential_mismatch is (which the app's own edits can never
// produce, so any occurrence there is definitely external tampering).
// Terminals are matched by index between the two linked elements: a
// report pair is authored as matching symbols carrying the same set
// of wires in the same declared order, the same convention terminal
// indexing already follows everywhere else in this API.
QSet<Element *> visited_report;
for (int f = 0; f < diagrams.count(); ++f) {
if (folioIndex >= 0 && f != folioIndex) continue;
DiagramContent content(diagrams.at(f), false);
for (Element *elmt : std::as_const(content.m_elements)) {
if (elmt->linkType() != Element::NextReport
&& elmt->linkType() != Element::PreviousReport) continue;
if (visited_report.contains(elmt)) continue;
const QList<Element *> linked = elmt->linkedElements();
for (Element *other : linked) {
if (visited_report.contains(other)) continue;
visited_report << elmt << other;
const QList<Terminal *> ta = elmt->terminals();
const QList<Terminal *> tb = other->terminals();
const int n = qMin(ta.count(), tb.count());
auto *other_diagram = qobject_cast<Diagram *>(other->scene());
const int other_folio = other_diagram ? diagrams.indexOf(other_diagram) : -1;
for (int i = 0; i < n; ++i) {
const QList<Conductor *> ca = ta.at(i)->conductors();
const QList<Conductor *> cb = tb.at(i)->conductors();
// ambiguous (>1) or unconnected (0) on either side:
// nothing to meaningfully compare
if (ca.count() != 1 || cb.count() != 1) continue;
for (const QString &prop : checked_properties) {
const QString va = conductorPropertyValue(ca.first()->properties(), prop);
const QString vb = conductorPropertyValue(cb.first()->properties(), prop);
if (va == vb) continue;
QVariantMap finding;
finding.insert(QStringLiteral("kind"), QStringLiteral("report_link_mismatch"));
finding.insert(QStringLiteral("severity"), QStringLiteral("warning"));
finding.insert(QStringLiteral("folio"), f);
finding.insert(QStringLiteral("element"), elmt->uuid().toString());
finding.insert(QStringLiteral("otherFolio"), other_folio);
finding.insert(QStringLiteral("otherElement"), other->uuid().toString());
finding.insert(QStringLiteral("terminal"), i);
finding.insert(QStringLiteral("property"), prop);
finding.insert(QStringLiteral("values"), QStringList{va, vb});
finding.insert(QStringLiteral("message"),
QStringLiteral("folio-link conductor disagrees on %1 across the "
"link: '%2' vs '%3'").arg(prop, va, vb));
findings << finding;
}
}
}
}
}
return findings;
}