From c9ac186a2026cd4b7fa94ea8ec2e16d4d4be02fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Wed, 3 Jun 2026 16:14:41 +0200 Subject: [PATCH] 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 --- DESIGN.md | 6 +++--- src/system/bsdl_check.cpp | 14 ++++++++++---- src/system/bsdl_check.hpp | 9 +++++++-- src/tui/commands.cpp | 4 ++-- src/tui/screen_analyze.cpp | 22 ++++++++++++++++++++-- src/tui/screen_dashboard.cpp | 9 +++++++++ 6 files changed, 51 insertions(+), 13 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index aef6df2..60064f4 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -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. -- **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. - **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. -**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) + `""` (bold) + `" — "` (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. - **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 diff --git a/src/system/bsdl_check.cpp b/src/system/bsdl_check.cpp index 93b6117..e200c41 100644 --- a/src/system/bsdl_check.cpp +++ b/src/system/bsdl_check.cpp @@ -39,13 +39,16 @@ std::string join_labels(const std::vector &pins) } // namespace -std::vector check_pin_specs(System *sys) +std::vector check_pin_specs(System *sys, const std::vector *nets) { std::vector out; if (!sys) return out; - for (const Net &net : compute_all_nets(sys)) { + std::vector local; + if (!nets) + local = compute_all_nets(sys); + for (const Net &net : (nets ? *nets : local)) { std::vector pins; std::vector sigs; Module *mod = nullptr; @@ -123,14 +126,17 @@ std::vector check_pin_specs(System *sys) return out; } -std::vector check_jtag_chain(System *sys) +std::vector check_jtag_chain(System *sys, const std::vector *nets_in) { std::vector out; if (!sys) return out; // Map every pin to the index of the net it sits on. - std::vector nets = compute_all_nets(sys); + std::vector local; + if (!nets_in) + local = compute_all_nets(sys); + const std::vector &nets = nets_in ? *nets_in : local; std::unordered_map net_of; for (size_t i = 0; i < nets.size(); ++i) for (auto &mp : nets[i].members) diff --git a/src/system/bsdl_check.hpp b/src/system/bsdl_check.hpp index 65af80a..dbc95b2 100644 --- a/src/system/bsdl_check.hpp +++ b/src/system/bsdl_check.hpp @@ -2,11 +2,16 @@ #define _BSDL_CHECK_HPP_ #include "analysis.hpp" // Anomaly, AnomalyKind +#include "nets.hpp" // Net #include 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 // direction/function populated by connector or BSDL models. Emits: // - 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. // Read-only; nets with no direction data are skipped (no false positives on // un-modelled parts). -std::vector check_pin_specs(System *sys); +std::vector check_pin_specs(System *sys, const std::vector *nets = nullptr); // 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: @@ -23,7 +28,7 @@ std::vector check_pin_specs(System *sys); // - JtagChainBreak : the TDO→TDI daisy chain dangles, fans out, or is not a // single path (≠1 head / ≠1 tail). // Empty when the system has no TAP pins. -std::vector check_jtag_chain(System *sys); +std::vector check_jtag_chain(System *sys, const std::vector *nets = nullptr); // 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 diff --git a/src/tui/commands.cpp b/src/tui/commands.cpp index 459a1ea..fd20707 100644 --- a/src/tui/commands.cpp +++ b/src/tui/commands.cpp @@ -300,14 +300,14 @@ void Tui::RegisterCommands() { // Model-driven pin checks (drive contention / undriven net / NC-wired) // 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) Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message); Print("verify: " + std::to_string(pin_anoms.size()) + " model-driven pin anomaly(ies)."); // 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) Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message); Print("verify: " + std::to_string(jtag_anoms.size()) diff --git a/src/tui/screen_analyze.cpp b/src/tui/screen_analyze.cpp index d3020dd..f03c92b 100644 --- a/src/tui/screen_analyze.cpp +++ b/src/tui/screen_analyze.cpp @@ -2,6 +2,7 @@ #include "tui/tui_helpers.hpp" #include "system/analysis.hpp" +#include "system/bsdl_check.hpp" #include "system/connect.hpp" #include "system/modules.hpp" #include "system/nets.hpp" @@ -99,16 +100,33 @@ Component Tui::BuildAnalyzeScreen() { + anomaly_kind_name(a.kind) + "] " + a.message); + // Model-driven checks (same as `verify`), reusing the nets above. + std::vector 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_issue_idx >= (int)analyze_issues.size()) analyze_issue_idx = (int)analyze_issues.size() - 1; std::string issues_header = "Issues (" + 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_inconsistent) + " net-mix, " - + std::to_string(rep.anomalies.size()) + " struct.)"; + + std::to_string(rep.anomalies.size()) + " struct, " + + std::to_string(n_model) + " model)"; // ============================================================ Groups analyze_groups.clear(); diff --git a/src/tui/screen_dashboard.cpp b/src/tui/screen_dashboard.cpp index 69223bf..69b4f79 100644 --- a/src/tui/screen_dashboard.cpp +++ b/src/tui/screen_dashboard.cpp @@ -2,6 +2,7 @@ #include "tui/tui_helpers.hpp" #include "system/analysis.hpp" +#include "system/bsdl_check.hpp" #include "system/connect.hpp" #include "system/modules.hpp" #include "system/nets.hpp" @@ -149,6 +150,14 @@ Component Tui::BuildDashboardScreen() { + std::to_string(orph_imported) + " imported, " + 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 ---- AnalysisReport rep = analyze_system(sys.get()); int n_diff = 0, n_diff_bus = 0, n_bus = 0;