P3.3: surface model anomalies in analyze + dashboard

The analyze screen's Issues pane now lists the model-driven checks
(check_pin_specs / check_jtag_chain / check_source_conflicts) alongside the
pin-role, net-mix and structural ones, with an "N model" count in the header;
the dashboard gains a "model:" health row. check_pin_specs/check_jtag_chain
take an optional precomputed net list, so verify, analyze and the dashboard
each compute the nets once and reuse them across checks instead of redoing the
transitive closure per check. Unit tests (75) green; verify output unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 16:14:41 +02:00
parent fe5b2c3d96
commit c9ac186a20
6 changed files with 51 additions and 13 deletions

View File

@@ -167,7 +167,7 @@ Exposed as the `analyze` shell command which prints groups (sorted by module + l
**Analyze screen** (`screen_analyze.cpp`, dashboard shortcut `a`, `screen_idx = 7`): unified **verify + analyze** view with a tabbed layout — horizontal tab bar at the top (`Issues (…) │ Groups (…) │ Types: …`), and a single scrollable detail panel below showing the active tab's list. Tab swap is handled at the outer `CatchEvent` (`Tab` / `→` cycle forward, `Shift-Tab` / `←` cycle back). The detail uses `Container::Tab({issues_menu, groups_menu, types_menu}, &analyze_focus_idx)` so `↑/↓` always navigate the visible list; each tab preserves its own selection idx. **Analyze screen** (`screen_analyze.cpp`, dashboard shortcut `a`, `screen_idx = 7`): unified **verify + analyze** view with a tabbed layout — horizontal tab bar at the top (`Issues (…) │ Groups (…) │ Types: …`), and a single scrollable detail panel below showing the active tab's list. Tab swap is handled at the outer `CatchEvent` (`Tab` / `→` cycle forward, `Shift-Tab` / `←` cycle back). The detail uses `Container::Tab({issues_menu, groups_menu, types_menu}, &analyze_focus_idx)` so `↑/↓` always navigate the visible list; each tab preserves its own selection idx.
- **Issues** pane merges: pin-role mismatches (typed pins whose actual signal type disagrees with the role from `connector_type`), bridged-net Power↔Gnd inconsistencies (the BFS check formerly in `verify` pass 2), and the structural anomalies from `analyze_system` (`DiffPairOrphan`, `BusGap`, `DiffBusGap`). Header counts each category. - **Issues** pane merges: pin-role mismatches (typed pins whose actual signal type disagrees with the role from `connector_type`), bridged-net Power↔Gnd inconsistencies (the BFS check formerly in `verify` pass 2), the structural anomalies from `analyze_system` (`DiffPairOrphan`, `BusGap`, `DiffBusGap`), and the model-driven checks (`check_pin_specs` / `check_jtag_chain` / `check_source_conflicts`, tagged `model` in the header). Header counts each category.
- **Groups** pane lists every detected `SignalGroup` sorted by `module / label` with kind tag and member count. - **Groups** pane lists every detected `SignalGroup` sorted by `module / label` with kind tag and member count.
- **Types** pane lists per-signal Power decisions (`[Power confirmed]` / `[Power REFUTED]` / `[Gnd]`) plus a trailing `[NC]` orphan rollup line. The pane header summarises counts (`N pwr-ok, M refuted, K gnd`). - **Types** pane lists per-signal Power decisions (`[Power confirmed]` / `[Power REFUTED]` / `[Gnd]`) plus a trailing `[NC]` orphan rollup line. The pane header summarises counts (`N pwr-ok, M refuted, K gnd`).
@@ -197,7 +197,7 @@ Today the only caller is `export` (`{"CSV", ".csv"}, {"ODS", ".ods"}` filters, k
**Command palette** (`screen_palette.cpp`): a global modal launcher attached to the whole tab tree via `tab | Modal(BuildPaletteModal(), &palette_open)` in `Run()`. Trigger: `Event::CtrlP` (FTXUI Input does not consume Ctrl-P, so the outer `CatchEvent` reliably picks it up first). Behaviour: a single Input bound to `palette_query` plus a result list rebuilt on every frame. Indexes three kinds of entries: commands (from the `commands` map), modules and per-module signals (qualified as `module/signal`). Fuzzy match is subsequence-based, case-insensitive: lower score wins, computed as `first_match_position * 100 + sum_of_gaps`. Kinds are biased by a constant offset (commands +0, modules +1000, signals +2000) so command matches come first when scores tie. Output capped at 20 rows to keep render cheap on big systems. Activation (`Enter`): commands → `Dispatch(name)` (which dispatches like the shell, including opening interactive screens), module → prefill `explore_*` state and jump to `screen_idx = 4`, signal → prefill `net_modules` + seed `net_sig_filter` to the exact signal name and jump to `screen_idx = 5`. `Esc` closes the palette. While the palette is open, the outer `CatchEvent` cedes events to it so Tab/Esc/etc. don't leak into the underlying screen. **Command palette** (`screen_palette.cpp`): a global modal launcher attached to the whole tab tree via `tab | Modal(BuildPaletteModal(), &palette_open)` in `Run()`. Trigger: `Event::CtrlP` (FTXUI Input does not consume Ctrl-P, so the outer `CatchEvent` reliably picks it up first). Behaviour: a single Input bound to `palette_query` plus a result list rebuilt on every frame. Indexes three kinds of entries: commands (from the `commands` map), modules and per-module signals (qualified as `module/signal`). Fuzzy match is subsequence-based, case-insensitive: lower score wins, computed as `first_match_position * 100 + sum_of_gaps`. Kinds are biased by a constant offset (commands +0, modules +1000, signals +2000) so command matches come first when scores tie. Output capped at 20 rows to keep render cheap on big systems. Activation (`Enter`): commands → `Dispatch(name)` (which dispatches like the shell, including opening interactive screens), module → prefill `explore_*` state and jump to `screen_idx = 4`, signal → prefill `net_modules` + seed `net_sig_filter` to the exact signal name and jump to `screen_idx = 5`. `Esc` closes the palette. While the palette is open, the outer `CatchEvent` cedes events to it so Tab/Esc/etc. don't leak into the underlying screen.
**Dashboard** (`screen_dashboard.cpp`, `dashboard` command, `screen_idx = 4`): read-only system overview. Single Renderer, no Input child. Recomputes everything per frame (cheap on realistic sizes): counters (modules/parts/signals/connections), three health rows (verify pin-role mismatches, bridged-net inconsistencies, NC orphans — green check / yellow warning prefix), an analysis summary line (diff pairs / buses / anomaly count, coloured if non-zero), and a per-module table (parts / signals / `connector_type`-tagged parts). Letter shortcuts handled in the outer `CatchEvent`: `c`=console, `p`=plug (connect), `t`=set-connector-type, `e`=explore, `a`=analyze, `q`=quit. `Esc` is swallowed on the dashboard (home). The dashboard is `interactive = true`, `scriptable = false`; running `dashboard` inside `source` aborts the script. **Dashboard** (`screen_dashboard.cpp`, `dashboard` command, `screen_idx = 4`): read-only system overview. Single Renderer, no Input child. Recomputes everything per frame (cheap on realistic sizes): counters (modules/parts/signals/connections), four health rows (verify pin-role mismatches, bridged-net inconsistencies, NC orphans, and BSDL/JTAG model anomalies — green check / yellow warning prefix), an analysis summary line (diff pairs / buses / anomaly count, coloured if non-zero), and a per-module table (parts / signals / `connector_type`-tagged parts). Letter shortcuts handled in the outer `CatchEvent`: `c`=console, `p`=plug (connect), `t`=set-connector-type, `e`=explore, `a`=analyze, `q`=quit. `Esc` is swallowed on the dashboard (home). The dashboard is `interactive = true`, `scriptable = false`; running `dashboard` inside `source` aborts the script.
**Screen titles** (shared idiom): every interactive screen renders a top bar in the form `" essim "` (bold) + `"→ "` (dim) + `"<screen-name>"` (bold) + `" — <short description>"` (dim), followed by a `separator()`. The main screen has its own variant that adds a live `N module(s), M connection(s)` counter on the right. Aim is to make the breadcrumb between essim and the current mode visible at all times. **Screen titles** (shared idiom): every interactive screen renders a top bar in the form `" essim "` (bold) + `"→ "` (dim) + `"<screen-name>"` (bold) + `" — <short description>"` (dim), followed by a `separator()`. The main screen has its own variant that adds a live `N module(s), M connection(s)` counter on the right. Aim is to make the breadcrumb between essim and the current mode visible at all times.
@@ -299,7 +299,7 @@ The analyze screen additionally surfaces two "verify-class" issues, computed the
- **pin-role mismatch** — a pin whose `expected_signal_type()` (derived from its `PinSpec`, set by `set-connector-type` via `pin_role(connector_type, pin_name)`) disagrees with the actual signal type. - **pin-role mismatch** — a pin whose `expected_signal_type()` (derived from its `PinSpec`, set by `set-connector-type` via `pin_role(connector_type, pin_name)`) disagrees with the actual signal type.
- **net-mix** — a bridged net (BFS over `Connection::pin_map`, ≥ 2 members) where `net_type_consistent(net, &dominant)` returns false. Specifically, the net contains both `Power` and `GndShield` signals. - **net-mix** — a bridged net (BFS over `Connection::pin_map`, ≥ 2 members) where `net_type_consistent(net, &dominant)` returns false. Specifically, the net contains both `Power` and `GndShield` signals.
The `verify` command (not the analyze screen, yet) also emits the **model-driven `AnomalyKind`s** from `bsdl_check.{hpp,cpp}`: `DriveContention` / `UndrivenNet` / `NcWired` (`check_pin_specs`) and `JtagTapIncomplete` / `JtagChainBreak` / `JtagBusUnbridged` (`check_jtag_chain`); and `SourceConflict` (`check_source_conflicts` — a BSDL power/ground pin the netlist leaves unconnected). They consume the BSDL-populated `PinSpec` plus `compute_all_nets`. Surfacing them in the analyze/dashboard Issues pane is a TODO. The `verify` command (not the analyze screen, yet) also emits the **model-driven `AnomalyKind`s** from `bsdl_check.{hpp,cpp}`: `DriveContention` / `UndrivenNet` / `NcWired` (`check_pin_specs`) and `JtagTapIncomplete` / `JtagChainBreak` / `JtagBusUnbridged` (`check_jtag_chain`); and `SourceConflict` (`check_source_conflicts` — a BSDL power/ground pin the netlist leaves unconnected). They consume the BSDL-populated `PinSpec` plus `compute_all_nets`, and are surfaced in three places: the `verify` command, the analyze screen's Issues pane (counted as `… N model`), and a `model:` health row on the dashboard. `check_pin_specs`/`check_jtag_chain` accept an optional precomputed net list, so verify, analyze and the dashboard each compute the nets once and reuse them across checks.
### Component kind ### Component kind

View File

@@ -39,13 +39,16 @@ std::string join_labels(const std::vector<Pin *> &pins)
} // namespace } // namespace
std::vector<Anomaly> check_pin_specs(System *sys) std::vector<Anomaly> check_pin_specs(System *sys, const std::vector<Net> *nets)
{ {
std::vector<Anomaly> out; std::vector<Anomaly> out;
if (!sys) if (!sys)
return out; return out;
for (const Net &net : compute_all_nets(sys)) { std::vector<Net> local;
if (!nets)
local = compute_all_nets(sys);
for (const Net &net : (nets ? *nets : local)) {
std::vector<Pin *> pins; std::vector<Pin *> pins;
std::vector<Signal *> sigs; std::vector<Signal *> sigs;
Module *mod = nullptr; Module *mod = nullptr;
@@ -123,14 +126,17 @@ std::vector<Anomaly> check_pin_specs(System *sys)
return out; return out;
} }
std::vector<Anomaly> check_jtag_chain(System *sys) std::vector<Anomaly> check_jtag_chain(System *sys, const std::vector<Net> *nets_in)
{ {
std::vector<Anomaly> out; std::vector<Anomaly> out;
if (!sys) if (!sys)
return out; return out;
// Map every pin to the index of the net it sits on. // Map every pin to the index of the net it sits on.
std::vector<Net> nets = compute_all_nets(sys); std::vector<Net> local;
if (!nets_in)
local = compute_all_nets(sys);
const std::vector<Net> &nets = nets_in ? *nets_in : local;
std::unordered_map<Pin *, int> net_of; std::unordered_map<Pin *, int> net_of;
for (size_t i = 0; i < nets.size(); ++i) for (size_t i = 0; i < nets.size(); ++i)
for (auto &mp : nets[i].members) for (auto &mp : nets[i].members)

View File

@@ -2,11 +2,16 @@
#define _BSDL_CHECK_HPP_ #define _BSDL_CHECK_HPP_
#include "analysis.hpp" // Anomaly, AnomalyKind #include "analysis.hpp" // Anomaly, AnomalyKind
#include "nets.hpp" // Net
#include <vector> #include <vector>
class System; class System;
// The net checks below accept an optional precomputed net list: callers that
// already have one (verify, the analyze screen, the dashboard) pass it so the
// transitive-closure pass isn't redone. Pass nullptr to compute it internally.
// Model-driven pin checks over the system's nets, using the PinSpec // Model-driven pin checks over the system's nets, using the PinSpec
// direction/function populated by connector or BSDL models. Emits: // direction/function populated by connector or BSDL models. Emits:
// - DriveContention : a net with ≥2 push-pull output drivers; // - DriveContention : a net with ≥2 push-pull output drivers;
@@ -14,7 +19,7 @@ class System;
// - NcWired : a no-connect pin wired onto a multi-pin net. // - NcWired : a no-connect pin wired onto a multi-pin net.
// Read-only; nets with no direction data are skipped (no false positives on // Read-only; nets with no direction data are skipped (no false positives on
// un-modelled parts). // un-modelled parts).
std::vector<Anomaly> check_pin_specs(System *sys); std::vector<Anomaly> check_pin_specs(System *sys, const std::vector<Net> *nets = nullptr);
// JTAG boundary-scan chain integrity, using pins whose PinSpec.function is a TAP // JTAG boundary-scan chain integrity, using pins whose PinSpec.function is a TAP
// role (JtagTdi/Tdo/Tms/Tck/Trst). Resolves each TAP pin to its net and checks: // role (JtagTdi/Tdo/Tms/Tck/Trst). Resolves each TAP pin to its net and checks:
@@ -23,7 +28,7 @@ std::vector<Anomaly> check_pin_specs(System *sys);
// - JtagChainBreak : the TDO→TDI daisy chain dangles, fans out, or is not a // - JtagChainBreak : the TDO→TDI daisy chain dangles, fans out, or is not a
// single path (≠1 head / ≠1 tail). // single path (≠1 head / ≠1 tail).
// Empty when the system has no TAP pins. // Empty when the system has no TAP pins.
std::vector<Anomaly> check_jtag_chain(System *sys); std::vector<Anomaly> check_jtag_chain(System *sys, const std::vector<Net> *nets = nullptr);
// Conflicts between a device model and the netlist's own view of a pin. Today: // Conflicts between a device model and the netlist's own view of a pin. Today:
// a pin the BSDL declares power/ground (a must-connect rail) that the netlist // a pin the BSDL declares power/ground (a must-connect rail) that the netlist

View File

@@ -300,14 +300,14 @@ void Tui::RegisterCommands() {
// Model-driven pin checks (drive contention / undriven net / NC-wired) // Model-driven pin checks (drive contention / undriven net / NC-wired)
// from the PinSpec direction/function populated by connector/BSDL models. // from the PinSpec direction/function populated by connector/BSDL models.
auto pin_anoms = check_pin_specs(sys.get()); auto pin_anoms = check_pin_specs(sys.get(), &nets);
for (const auto &a : pin_anoms) 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(pin_anoms.size()) Print("verify: " + std::to_string(pin_anoms.size())
+ " model-driven pin anomaly(ies)."); + " model-driven pin anomaly(ies).");
// JTAG boundary-scan chain integrity (TAP pins → nets). // JTAG boundary-scan chain integrity (TAP pins → nets).
auto jtag_anoms = check_jtag_chain(sys.get()); auto jtag_anoms = check_jtag_chain(sys.get(), &nets);
for (const auto &a : jtag_anoms) for (const auto &a : jtag_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(jtag_anoms.size()) Print("verify: " + std::to_string(jtag_anoms.size())

View File

@@ -2,6 +2,7 @@
#include "tui/tui_helpers.hpp" #include "tui/tui_helpers.hpp"
#include "system/analysis.hpp" #include "system/analysis.hpp"
#include "system/bsdl_check.hpp"
#include "system/connect.hpp" #include "system/connect.hpp"
#include "system/modules.hpp" #include "system/modules.hpp"
#include "system/nets.hpp" #include "system/nets.hpp"
@@ -99,16 +100,33 @@ Component Tui::BuildAnalyzeScreen() {
+ anomaly_kind_name(a.kind) + "] " + anomaly_kind_name(a.kind) + "] "
+ a.message); + a.message);
// Model-driven checks (same as `verify`), reusing the nets above.
std::vector<Anomaly> model_anoms;
{
auto a1 = check_pin_specs(sys.get(), &nets);
auto a2 = check_jtag_chain(sys.get(), &nets);
auto a3 = check_source_conflicts(sys.get());
model_anoms.insert(model_anoms.end(), a1.begin(), a1.end());
model_anoms.insert(model_anoms.end(), a2.begin(), a2.end());
model_anoms.insert(model_anoms.end(), a3.begin(), a3.end());
}
for (const auto &a : model_anoms)
analyze_issues.push_back(std::string("[")
+ anomaly_kind_name(a.kind) + "] "
+ a.message);
int n_model = (int)model_anoms.size();
if (analyze_issues.empty()) analyze_issues.push_back("(no issue found)"); if (analyze_issues.empty()) analyze_issues.push_back("(no issue found)");
if (analyze_issue_idx >= (int)analyze_issues.size()) if (analyze_issue_idx >= (int)analyze_issues.size())
analyze_issue_idx = (int)analyze_issues.size() - 1; analyze_issue_idx = (int)analyze_issues.size() - 1;
std::string issues_header = "Issues (" std::string issues_header = "Issues ("
+ std::to_string(n_role_mismatches + n_inconsistent + std::to_string(n_role_mismatches + n_inconsistent
+ (int)rep.anomalies.size()) + (int)rep.anomalies.size() + n_model)
+ ": " + std::to_string(n_role_mismatches) + " pin-role, " + ": " + std::to_string(n_role_mismatches) + " pin-role, "
+ std::to_string(n_inconsistent) + " net-mix, " + std::to_string(n_inconsistent) + " net-mix, "
+ std::to_string(rep.anomalies.size()) + " struct.)"; + std::to_string(rep.anomalies.size()) + " struct, "
+ std::to_string(n_model) + " model)";
// ============================================================ Groups // ============================================================ Groups
analyze_groups.clear(); analyze_groups.clear();

View File

@@ -2,6 +2,7 @@
#include "tui/tui_helpers.hpp" #include "tui/tui_helpers.hpp"
#include "system/analysis.hpp" #include "system/analysis.hpp"
#include "system/bsdl_check.hpp"
#include "system/connect.hpp" #include "system/connect.hpp"
#include "system/modules.hpp" #include "system/modules.hpp"
#include "system/nets.hpp" #include "system/nets.hpp"
@@ -149,6 +150,14 @@ Component Tui::BuildDashboardScreen() {
+ std::to_string(orph_imported) + " imported, " + std::to_string(orph_imported) + " imported, "
+ std::to_string(orph_dropped) + " dropped)")); + std::to_string(orph_dropped) + " dropped)"));
// Model-driven checks (BSDL pin specs, JTAG chain, source conflicts),
// reusing the nets computed above.
int n_model = (int)(check_pin_specs(sys.get(), &nets).size()
+ check_jtag_chain(sys.get(), &nets).size()
+ check_source_conflicts(sys.get()).size());
health_rows.push_back(health_line(n_model == 0,
"model: " + std::to_string(n_model) + " BSDL/JTAG anomaly(ies)"));
// ---- analysis summary ---- // ---- analysis summary ----
AnalysisReport rep = analyze_system(sys.get()); AnalysisReport rep = analyze_system(sys.get());
int n_diff = 0, n_diff_bus = 0, n_bus = 0; int n_diff = 0, n_diff_bus = 0, n_bus = 0;