Extract verify into core (app::verify); thin the TUI command.

Move the 7-pass verify orchestration out of the TUI command lambda and
into core/app/verify.{hpp,cpp}: app::verify(System*) returns a structured
VerifyReport (role mismatches, net inconsistencies, orphan counts, the four
model-driven anomaly vectors) with no Print/dialog/FTXUI. The nets are
computed once and fed to the net-based checks.

The verify command is now a thin renderer over the report, byte-identical
output. Prune the now-dead nets.hpp / bsdl_check.hpp / <unordered_set>
includes from commands.cpp.

Add tests/test_verify.cpp: builds small systems by hand and asserts the
report (empty system, Power/GndShield bridged-net inconsistency, orphan
counts by import origin) — pure core, no UI.

This is the structuring extraction: the same VerifyReport can now back the
analyze screen's Issues pane and the dashboard health rows, removing the
triple duplication of passes 1-3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 19:51:53 +02:00
parent cccc5f131d
commit e3350b8d95
4 changed files with 272 additions and 95 deletions

98
src/core/app/verify.cpp Normal file
View File

@@ -0,0 +1,98 @@
#include "core/app/verify.hpp"
#include "core/domain/bsdl_check.hpp"
#include "core/domain/connect.hpp"
#include "core/domain/modules.hpp"
#include "core/domain/nets.hpp"
#include "core/domain/parts.hpp"
#include "core/domain/pins.hpp"
#include "core/domain/signals.hpp"
#include "core/domain/system.hpp"
#include <unordered_set>
#include <utility>
#include <vector>
namespace app {
VerifyReport verify(System *sys)
{
VerifyReport r;
if (!sys)
return r;
// Pass 1 — typed pins: expected (model) vs actual (net) signal type.
for (auto &mkv : *sys->modules()) {
Module *mod = mkv.second;
for (auto &pkv : *mod) {
Part *prt = pkv.second;
if (prt->connector_type.empty())
continue;
for (auto &nkv : *prt) {
Pin *pin = nkv.second;
++r.typed_pins;
SignalType expected = pin->expected_signal_type();
if (expected == SignalType::Other)
continue;
Signal *s = pin->signal();
SignalType actual = s ? s->type : SignalType::Other;
if (actual == expected)
continue;
RoleMismatch m;
m.module = mod->name;
m.part = prt->name;
m.pin = pin->name;
m.signal = s ? s->name : std::string("(NC)");
m.expected = expected;
m.actual = actual;
r.role_mismatches.push_back(std::move(m));
}
}
}
// Pass 2 — bridged nets: flag Power/GndShield mixing. Compute the nets once
// here and reuse them for the model checks below.
std::vector<Net> nets = compute_all_nets(sys);
r.total_nets = (int)nets.size();
for (const Net &n : nets) {
if (n.members.size() < 2)
continue;
++r.bridged_nets;
SignalType dom;
if (net_type_consistent(n, dom))
continue;
NetInconsistency ni;
for (const auto &mp : n.members)
ni.members.push_back({mp.first->name, mp.second->name, mp.second->type});
r.net_inconsistencies.push_back(std::move(ni));
}
// Pass 3 — orphans: pins with no signal and not bridged via a connection.
std::unordered_set<Pin *> bridged_pins;
for (auto &ckv : *sys->connections())
for (auto &wp : ckv.second->pin_map) {
if (wp.first) bridged_pins.insert(wp.first);
if (wp.second) bridged_pins.insert(wp.second);
}
for (auto &mkv : *sys->modules())
for (auto &pkv : *mkv.second)
for (auto &nkv : *pkv.second) {
Pin *pin = nkv.second;
if (pin->signal() || bridged_pins.count(pin))
continue;
if (pin->nc_origin == NcOrigin::ImportedUnconnected)
++r.orphan_imported;
else if (pin->nc_origin == NcOrigin::DroppedSingleton)
++r.orphan_dropped;
}
// Passes 4-7 — model-driven checks (reuse the nets from pass 2).
r.pin_anomalies = check_pin_specs(sys, &nets);
r.jtag_anomalies = check_jtag_chain(sys, &nets);
r.conflict_anomalies = check_source_conflicts(sys);
r.completeness_anomalies = check_bsdl_completeness(sys);
return r;
}
} // namespace app

61
src/core/app/verify.hpp Normal file
View File

@@ -0,0 +1,61 @@
#ifndef _APP_VERIFY_HPP_
#define _APP_VERIFY_HPP_
#include "core/domain/analysis.hpp" // Anomaly
#include "core/domain/signal_type.hpp" // SignalType
#include <string>
#include <vector>
class System;
namespace app {
// One typed-pin role mismatch: the connector/BSDL expectation disagrees with
// the actual net type.
struct RoleMismatch {
std::string module, part, pin;
std::string signal; ///< signal name, or "(NC)"
SignalType expected = SignalType::Other;
SignalType actual = SignalType::Other;
};
// One bridged net mixing Power and GndShield, with its members for display.
struct NetInconsistency {
struct Member { std::string module, signal; SignalType type; };
std::vector<Member> members;
};
// The full result of `verify`: structured data only — no strings beyond the
// names, no formatting. Frontends (the verify command, the analyze screen, the
// dashboard) render it however they like.
struct VerifyReport {
int typed_pins = 0; ///< pins with a non-Other expectation considered
std::vector<RoleMismatch> role_mismatches;
int total_nets = 0;
int bridged_nets = 0;
std::vector<NetInconsistency> net_inconsistencies;
int orphan_imported = 0;
int orphan_dropped = 0;
std::vector<Anomaly> pin_anomalies; ///< check_pin_specs
std::vector<Anomaly> jtag_anomalies; ///< check_jtag_chain
std::vector<Anomaly> conflict_anomalies; ///< check_source_conflicts
std::vector<Anomaly> completeness_anomalies; ///< check_bsdl_completeness
int orphan_total() const { return orphan_imported + orphan_dropped; }
int model_total() const {
return (int)(pin_anomalies.size() + jtag_anomalies.size()
+ conflict_anomalies.size() + completeness_anomalies.size());
}
};
// Run every verify pass over the system and return the findings. Pure core —
// computes the nets once and feeds them to the net-based checks.
VerifyReport verify(System *sys);
} // namespace app
#endif // _APP_VERIFY_HPP_

View File

@@ -4,16 +4,16 @@
#include "core/domain/analysis.hpp" #include "core/domain/analysis.hpp"
#include "core/domain/connect.hpp" #include "core/domain/connect.hpp"
#include "core/domain/modules.hpp" #include "core/domain/modules.hpp"
#include "core/domain/nets.hpp"
#include "core/domain/parts.hpp" #include "core/domain/parts.hpp"
#include "core/domain/persist.hpp" #include "core/domain/persist.hpp"
#include "core/domain/pin_role.hpp" #include "core/domain/pin_role.hpp"
#include "core/domain/pin_model.hpp" #include "core/domain/pin_model.hpp"
#include "core/domain/bsdl_model.hpp" #include "core/domain/bsdl_model.hpp"
#include "core/domain/bsdl_check.hpp"
#include "core/domain/pins.hpp" #include "core/domain/pins.hpp"
#include "core/domain/signals.hpp" #include "core/domain/signals.hpp"
#include "core/domain/system.hpp" #include "core/domain/system.hpp"
#include "core/app/verify.hpp"
#include "core/domain/transform.hpp" #include "core/domain/transform.hpp"
#include "core/domain/transform_vpx.hpp" #include "core/domain/transform_vpx.hpp"
@@ -22,7 +22,6 @@
#include <cstdlib> #include <cstdlib>
#include <exception> #include <exception>
#include <fstream> #include <fstream>
#include <unordered_set>
#include <utility> #include <utility>
void Tui::RegisterCommands() { void Tui::RegisterCommands() {
@@ -228,104 +227,43 @@ void Tui::RegisterCommands() {
commands["verify"] = { {}, [this](auto &) { commands["verify"] = { {}, [this](auto &) {
if (!sys) { Print("no system: run 'new' first."); return; } if (!sys) { Print("no system: run 'new' first."); return; }
int checked = 0; app::VerifyReport r = app::verify(sys.get());
int mismatches = 0;
for (auto &mkv : *sys->modules()) {
Module *mod = mkv.second;
for (auto &pkv : *mod) {
Part *prt = pkv.second;
if (prt->connector_type.empty()) continue;
for (auto &nkv : *prt) {
Pin *pin = nkv.second;
++checked;
SignalType expected = pin->expected_signal_type();
if (expected == SignalType::Other) continue;
Signal *s = pin->signal();
SignalType actual = s ? s->type : SignalType::Other;
if (actual == expected) continue;
++mismatches;
std::string sig_label = s ? s->name : std::string("(NC)");
Print(" " + mod->name + "/" + prt->name + "/" + pin->name
+ ": expected " + signal_type_name(expected)
+ ", got " + signal_type_name(actual)
+ " (signal: " + sig_label + ")");
}
}
}
Print("verify: " + std::to_string(mismatches) + " local mismatch(es) over "
+ std::to_string(checked) + " typed pin(s).");
auto nets = compute_all_nets(sys.get()); for (const auto &m : r.role_mismatches)
int bridged = 0, inconsistent = 0; Print(" " + m.module + "/" + m.part + "/" + m.pin
for (const auto &n : nets) { + ": expected " + signal_type_name(m.expected)
if (n.members.size() < 2) continue; + ", got " + signal_type_name(m.actual)
++bridged; + " (signal: " + m.signal + ")");
SignalType dom; Print("verify: " + std::to_string(r.role_mismatches.size())
if (net_type_consistent(n, dom)) continue; + " local mismatch(es) over " + std::to_string(r.typed_pins)
++inconsistent; + " typed pin(s).");
for (const auto &ni : r.net_inconsistencies) {
std::string line = " net mixes Power and GndShield:"; std::string line = " net mixes Power and GndShield:";
for (const auto &mp : n.members) { for (const auto &mem : ni.members)
line += " " + mp.first->name + "/" + mp.second->name line += " " + mem.module + "/" + mem.signal
+ "(" + signal_type_name(mp.second->type) + ")"; + "(" + signal_type_name(mem.type) + ")";
}
Print(line); Print(line);
} }
Print("verify: " + std::to_string(inconsistent) + " inconsistent net(s) over " Print("verify: " + std::to_string(r.net_inconsistencies.size())
+ std::to_string(bridged) + " bridged net(s) (" + " inconsistent net(s) over " + std::to_string(r.bridged_nets)
+ std::to_string(nets.size()) + " total)."); + " bridged net(s) (" + std::to_string(r.total_nets) + " total).");
// Orphan pin report. A pin is "orphan" if it came out of import (or Print("verify: " + std::to_string(r.orphan_total())
// post-import drop) with no signal, and is still not bridged to a + " orphan pin(s) at import (" + std::to_string(r.orphan_imported)
// real signal via any Connection::pin_map. Use `nc-export` for the + " imported NC, " + std::to_string(r.orphan_dropped)
// per-pin list. + " dropped singleton).");
std::unordered_set<Pin *> bridged_pins;
for (auto &ckv : *sys->connections())
for (auto &wp : ckv.second->pin_map) {
if (wp.first) bridged_pins.insert(wp.first);
if (wp.second) bridged_pins.insert(wp.second);
}
int orph_imported = 0, orph_dropped = 0;
for (auto &mkv : *sys->modules())
for (auto &pkv : *mkv.second)
for (auto &nkv : *pkv.second) {
Pin *pin = nkv.second;
if (pin->signal() || bridged_pins.count(pin)) continue;
if (pin->nc_origin == NcOrigin::ImportedUnconnected) ++orph_imported;
else if (pin->nc_origin == NcOrigin::DroppedSingleton) ++orph_dropped;
}
Print("verify: " + std::to_string(orph_imported + orph_dropped)
+ " orphan pin(s) at import ("
+ std::to_string(orph_imported) + " imported NC, "
+ std::to_string(orph_dropped) + " dropped singleton).");
// Model-driven pin checks (drive contention / undriven net / NC-wired) // Each model-driven group: per-finding lines + a one-line summary.
// from the PinSpec direction/function populated by connector/BSDL models. auto render = [this](const std::vector<Anomaly> &v, const char *tail) {
auto pin_anoms = check_pin_specs(sys.get(), &nets); for (const auto &a : v)
for (const auto &a : pin_anoms) Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message);
Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message); Print("verify: " + std::to_string(v.size()) + tail);
Print("verify: " + std::to_string(pin_anoms.size()) };
+ " model-driven pin anomaly(ies)."); render(r.pin_anomalies, " model-driven pin anomaly(ies).");
render(r.jtag_anomalies, " JTAG chain anomaly(ies).");
// JTAG boundary-scan chain integrity (TAP pins → nets). render(r.conflict_anomalies, " source-conflict(s).");
auto jtag_anoms = check_jtag_chain(sys.get(), &nets); render(r.completeness_anomalies, " BSDL completeness issue(s).");
for (const auto &a : jtag_anoms)
Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message);
Print("verify: " + std::to_string(jtag_anoms.size())
+ " JTAG chain anomaly(ies).");
// Model-vs-netlist conflicts (e.g. a BSDL power pin left unconnected).
auto conflict_anoms = check_source_conflicts(sys.get());
for (const auto &a : conflict_anoms)
Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message);
Print("verify: " + std::to_string(conflict_anoms.size())
+ " source-conflict(s).");
// BSDL completeness: device power/ground pins missing from the netlist.
auto missing_anoms = check_bsdl_completeness(sys.get());
for (const auto &a : missing_anoms)
Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message);
Print("verify: " + std::to_string(missing_anoms.size())
+ " BSDL completeness issue(s).");
}, true, }, true,
"check pin roles, power/gnd net consistency, and (with BSDL) pin and JTAG checks" }; "check pin roles, power/gnd net consistency, and (with BSDL) pin and JTAG checks" };

80
tests/test_verify.cpp Normal file
View File

@@ -0,0 +1,80 @@
#include <doctest/doctest.h>
#include "core/app/verify.hpp"
#include "core/domain/connect.hpp"
#include "core/domain/modules.hpp"
#include "core/domain/parts.hpp"
#include "core/domain/pins.hpp"
#include "core/domain/signals.hpp"
#include "core/domain/system.hpp"
// app::verify is pure core: it takes a System* and returns a VerifyReport of
// structured findings, with no Print/dialog/FTXUI. These tests build small
// systems by hand and assert the report — no UI involved.
TEST_CASE("verify on a null or empty system reports nothing") {
app::VerifyReport none = app::verify(nullptr);
CHECK(none.typed_pins == 0);
CHECK(none.total_nets == 0);
CHECK(none.role_mismatches.empty());
System sys;
app::VerifyReport r = app::verify(&sys);
CHECK(r.typed_pins == 0);
CHECK(r.total_nets == 0);
CHECK(r.bridged_nets == 0);
CHECK(r.net_inconsistencies.empty());
CHECK(r.orphan_total() == 0);
CHECK(r.model_total() == 0);
}
TEST_CASE("verify flags a bridged net that mixes Power and GndShield") {
// Two cards, one wired pin pair: A.NETA (Power) <-> B.NETB (GndShield).
System sys;
Module *a = sys.modules()->merge("A");
Module *b = sys.modules()->merge("B");
Part *ja = new Part("J1"); a->add(ja);
Part *jb = new Part("P1"); b->add(jb);
Pin *pa = new Pin("1"); ja->add(pa);
Pin *pb = new Pin("1"); jb->add(pb);
Signal *sa = a->signals->merge("NETA"); sa->type = SignalType::Power;
Signal *sb = b->signals->merge("NETB"); sb->type = SignalType::GndShield;
sa->add(pa); pa->connect(sa);
sb->add(pb); pb->connect(sb);
Connection *c = new Connection("A.J1<->B.P1", a, ja, b, jb);
c->transform_name = "identity";
c->pin_map.emplace_back(pa, pb);
sys.connections()->add(c);
app::VerifyReport r = app::verify(&sys);
CHECK(r.total_nets == 1);
CHECK(r.bridged_nets == 1);
REQUIRE(r.net_inconsistencies.size() == 1);
CHECK(r.net_inconsistencies[0].members.size() == 2);
// Both endpoints are present with their declared types.
bool seen_power = false, seen_gnd = false;
for (const auto &m : r.net_inconsistencies[0].members) {
if (m.type == SignalType::Power) seen_power = true;
if (m.type == SignalType::GndShield) seen_gnd = true;
}
CHECK(seen_power);
CHECK(seen_gnd);
}
TEST_CASE("verify counts orphan pins by their import origin") {
System sys;
Module *m = sys.modules()->merge("M");
Part *p = new Part("J1"); m->add(p);
Pin *imp = new Pin("1"); imp->nc_origin = NcOrigin::ImportedUnconnected; p->add(imp);
Pin *drp = new Pin("2"); drp->nc_origin = NcOrigin::DroppedSingleton; p->add(drp);
Pin *wired = new Pin("3"); p->add(wired);
Signal *s = m->signals->merge("NET"); s->add(wired); wired->connect(s);
app::VerifyReport r = app::verify(&sys);
CHECK(r.orphan_imported == 1);
CHECK(r.orphan_dropped == 1);
CHECK(r.orphan_total() == 2);
}