# essim — notes for Claude System digital twin: simulator for the interconnections between cards/boards. C++17, FTXUI for the TUI, importers for external netlist formats. ## Build ```sh cmake -S . -B build cmake --build build -j ./build/essim ``` - CMake **3.14+** required (uses `FetchContent_MakeAvailable`). - FTXUI is fetched at configure time from GitHub (`v6.1.9`, shallow clone). First configure pays ~20 s for the clone; subsequent ones are cached in `build/_deps/`. - **System dependencies** (resolved via `find_package`): `libzip` (target `libzip::zip`) and `pugixml` (target `pugixml::pugixml`). Used by the ODS importer. Arch: `pacman -S libzip pugixml`; Debian/Ubuntu: `libzip-dev libpugixml-dev`. - **`libbsdl`** (standalone BSDL parser, LGPL-2.1) is the sibling repo at `../libbsdl`, pulled in via `add_subdirectory` (path overridable with `-DBSDL_DIR=...`) and linked dynamically (`bsdl::bsdl`; an LGPL `.so` is fine from EUPL essim). Powers the BSDL ingest behind `attach-bsdl`. - Sources are collected with `file(GLOB_RECURSE ALL_SOURCES "src/*.cpp")`. **After adding a new `.cpp`, re-run `cmake -S . -B build`** — CMake does not re-glob automatically and link will fail with "undefined reference". - **Headless / batch**: `essim --batch --source FILE` runs a script and prints its console output to stdout, then exits without the TUI (good for CI / capturing `verify`). Also `--restore FILE` and `--commands-md [FILE]`. `BootDispatch` runs `--restore`/`--source` synchronously before the event loop (`Source` takes its headless drain branch when no screen is attached), so the console buffer is complete by the time `--batch` dumps it (`Tui::DumpOutput`). ## Layout ``` src/ main.cpp -- launches Tui system/ -- domain model syselmts.hpp SystemElement + SystemElementContainer (templated, get/merge/iterate) modules.{hpp,cpp} Module, Modules parts.{hpp,cpp} Part (carries `kind` + `connector_type`), Parts pins.{hpp,cpp} Pin (carries PinSpec `spec`), Pins signals.{hpp,cpp} Signal, Signals signal_type.hpp SignalType + helpers pin_spec.hpp PinSpec (function/direction/pad/source) + SignalType mapping component_kind.{hpp,cpp} ComponentKind enum + infer_component_kind(name) pin_name.{hpp,cpp} canonical_pin_name(s) — zero-pad digit suffix to 3 connect.{hpp,cpp} Connection, Connections transform.{hpp,cpp} Transform / IdentityTransform / TransformRegistry + CheckIdentityCompatible + FillIdentityNCs pin_role.{hpp,cpp} pin_role(kind, name) → PinSpec, pin_layout(kind), FillPartFromLayout(part, kind) pin_model.{hpp,cpp} PinModel + apply_model(Part*, model) + ConnectorModel bsdl_model.{hpp,cpp} BsdlModel (libbsdl wrapper) + BsdlPinModel + apply_bsdl bsdl_check.{hpp,cpp} check_pin_specs / check_jtag_chain → vector nets.{hpp,cpp} find_net / compute_all_nets / net_type_consistent analysis.{hpp,cpp} analyze_system → AnalysisReport (diff pairs, buses, anomalies) persist.{hpp,cpp} save / restore (tab-delimited) system.{hpp,cpp} System: owns Modules + Connections, exposes Load() imports/ -- adapters that populate or emit the domain import_base.hpp ImportBase interface import_mentor.{hpp,cpp} Mentor Graphics netlist parser import_altium.{hpp,cpp} Altium netlist parser (`[ ]` parts, `( )` signals) import_ods.{hpp,cpp} ODS spreadsheet pinout (libzip + pugixml) ods_writer.{hpp,cpp} Minimal .ods writer (multi-sheet, string cells) tui/ -- FTXUI shell, split by responsibility tui.{hpp,cpp} Class Tui (state + Run() orchestrator + screen-mode event dispatcher) tui_helpers.{hpp,cpp} Free helpers: ToLower, NaturalLess, LongestCommonPrefix shell.cpp Print, Submit, Dispatch, Finalize, Tokenize, history persistence completion.cpp CompleteCommand, CompletePath, CompleteInline commands.cpp RegisterCommands (orchestrator + lifecycle / shell / topology commands) commands_export.cpp RegisterExportCommands (export → CSV / ODS, file-dialog hook) screen_main.cpp BuildMainScreen (visualisation area + bottom input) screen_connect.cpp BuildConnectScreen + shared RefreshFilteredPartList helper screen_settype.cpp BuildSettypeScreen screen_explore.cpp BuildExploreScreen (browse modules → parts/signals/connections → details; not scriptable) screen_dashboard.cpp BuildDashboardScreen (read-only system overview) screen_analyze.cpp BuildAnalyzeScreen (anomalies / groups / power decisions) screen_palette.cpp BuildPaletteModal (global Ctrl-P fuzzy launcher) screen_filedialog.cpp BuildFileDialog + OpenFileDialog (reusable file picker) screen_error.cpp BuildErrorModal + ShowError (centred error popup) screen_help.cpp BuildHelpScreen (topic-driven feature reference) screen_sigtype_modal.cpp BuildSignalTypeModal (popup attached to explore via Modal()) doc/classes.puml -- PlantUML class diagram ``` `include/` and `lib/` are kept empty by design — FTXUI used to live there as precompiled `.a` + headers, now it comes through FetchContent. ## Domain conventions - Everything in `system/` is **pointer-based** (commit `d8122d1`: "everything is pointer"). Containers store `T*`, ownership lives with the container. - `SystemElementContainer::merge(name)` is the get-or-create primitive — call it instead of `add` when you don't know whether the element already exists. `add` throws on duplicate names or empty names. - `using namespace std;` is present in `syselmts.hpp` — pre-existing, don't add more `using namespace` in headers. - Include guards `_NAME_HPP_` *and* `#pragma once` are both used. Match the existing style. ## TUI `Tui::Run()` enters an FTXUI fullscreen loop: - Top: scrollable visualisation area (`window` + `vscroll_indicator | yframe`, last line gets `focus` so it auto-scrolls). - Bottom: single-line `Input` inside a border. Label switches between `> ` (normal) and `? ` (during a multi-step prompt). - `CatchEvent` is wrapped **outside** the `Renderer` (not between `Renderer` and `Input`). Pattern: `Renderer(input_component, lambda)` then `CatchEvent(renderer, handler)`. Wrapping `CatchEvent(input, …)` inside a `Container::Vertical({…})` and then `Renderer(container, …)` was found to break the `Input`'s content rendering on at least one terminal — typed characters didn't show even though `on_enter` fired correctly. - `InputOption::transform` is overridden to avoid hard-coded colors (default sets `White on Black` when focused, which is unreadable on light terminal themes). The custom transform only applies `dim` to the placeholder; everything else inherits terminal default fg/bg, so the UI adapts to any theme. - `InputOption::multiline` **must** be set to `false` for the command prompt. Default is `true`, and FTXUI's `HandleReturn()` then both inserts `\n` into the content **and** fires `on_enter` — so `Submit()` would receive `"help\n"` instead of `"help"` and command lookups would all fall through to "unknown command". - Commands live in a `std::map` registry built in `RegisterCommands()`. Each `CommandSpec` carries `params` (list of `Param{name, completion}` where `completion` is the `Tui::Completion` enum: `None | Path | Command`), an `action(args)` lambda, a `prompt_for_missing` bool (default `true`), a one-line `description` string, a `scriptable` bool (default `true`), and an `interactive` bool (default `false`) that flags screen-opening commands. Setting `scriptable = false` (e.g. `explore`) excludes the command from the script-save buffer; if a non-scriptable command is invoked from a sourced file the `Source` loop still aborts via the `screen_idx != 0` check, since these commands are typically screen-openers. `interactive = true` is consumed only by `help`: the listing splits commands into two sections (`Interactive (open a full-screen mode)` and `Other`), and `help ` adds an `[interactive]` tag plus a note about the bare-vs-inline duality. `Dispatch(raw)` tokenises the input (whitespace split with `"…"` quoting), takes inline args first, and (only when `prompt_for_missing`) pushes a `Prompt` onto the queue for each missing param. The last prompt's callback calls `Finalize()`. Set `prompt_for_missing = false` for commands that accept either zero args or a full arg list (e.g. `search`: bare → interactive, full → inline) — the action gets called immediately with whatever it received and decides what to do. - `Tab` completes by `Param::completion`: `Path` triggers `CompletePath()` (filesystem listing with `~/` expansion), `Command` triggers `CompleteCommand()` (matches against the registry), `None` does nothing. Both work inline (`CompleteInline()` figures out the arg position) and inside a multi-step prompt. - `help` (no args) iterates the registry and prints `name — description` for every command. `help ` describes a single command, including each `Param`'s name. The argument has `Completion::Command`, so `help ` lists registered commands. - `Finalize()` rebuilds the **canonical inline form** (`name arg1 "arg with spaces" arg3`) and writes that to history — so a `load` command answered via interactive prompts still shows up in `history` (and on disk) as a single inline line, ready to be replayed via ↑. - Multi-step prompts work via a `std::deque` queue. `Submit()` pops them one by one before falling back to dispatch. Adding a new command = one entry in `RegisterCommands()`; the prompt-flow and inline-flow are both handled automatically. - Tab completion: at the top-level prompt (no `pending`), completes built-in command names. Inside a prompt with `path_completion = true` (e.g. the `filename` step of `load`), completes file paths via `std::filesystem::directory_iterator` (handles `~/`, dirs get a trailing `/`). Logic: 1 match → replace; multiple with progress on the longest common prefix → extend; multiple stuck at LCP → list candidates in the visualisation area. Built-in commands: `new`, `set`, `load`, `duplicate`, `save`, `restore`, `source`, `script-save`, `connect` (alias `plug`), `set-connector-type`, `attach-bsdl`, `set-signal-type`, `explore`, `verify`, `analyze`, `dashboard`, `clear`, `help`, `quit`/`exit`. `Esc` cancels an in-progress multi-step prompt. `set ` declares a session-scoped variable. Subsequent commands expand `$name` and `${name}` in their args (substitution happens in `Finalize` between canonical-form recording and `spec.action(args)` — so `history` and `script-save` keep the **unexpanded** form, while the action sees resolved values). Unknown variables are left literal. `vars` is reset by `new`. Validation: `[A-Za-z_][A-Za-z0-9_]*`. `duplicate ` deep-copies a module: signals (with type overrides), parts (with `connector_type` and `kind`), pins (with their `PinSpec` `spec`), and rewires each pin to the equivalent same-named signal in the new module. Connections are NOT copied (they're cross-module topology). `script-save ` writes a replay-ready script of every command issued since the last `new` (the `recorded` buffer is cleared inside the `new` action). The buffer is appended to inside `Finalize()` after the action runs, with a denylist of commands that aren't useful in a replay: `clear`, `help`, `quit`, `exit`, `source`, `script-save`. **Additionally, a bare invocation of an `interactive` command is skipped** (`opens_screen = spec.interactive && args.empty()`) — those open a full-screen mode rather than mutating state. Mutating actions taken *inside* a screen record their own canonical line (e.g. the signal-type popup pushes `set-signal-type `). Note the `source` exclusion: when a script is sourced, the *individual lines inside* the script are recorded (because each goes through `Finalize`), so the saved script reproduces the same end state without the indirection. `source ` reads a script line by line and feeds each line through `Submit()`. While the script is running, `in_source = true` is set on the `Tui` and: - `Dispatch` / `Finalize` skip writing to memory + on-disk history. - After each `Submit`, if `screen_idx != 0` (a screen was opened by an "interactive" command like bare `connect` / `explore` / `set-connector-type`), the script is aborted with an error message and `screen_idx` is reset to 0 — interactive screen-opening commands are explicitly disallowed in scripts. Pending prompts (from incomplete inline commands) are NOT considered interactive and are filled by subsequent script lines, the way you'd expect. Lines starting with `#` and blank lines are skipped; leading/trailing whitespace is trimmed; `~/` is expanded. `save` / `restore` (`src/system/persist.{hpp,cpp}`): tab-delimited line format, no extra deps. Tags: `M` (module), `P` (part with `connector_type`), `B` (part's attached BSDL `.bsd` path — re-parsed and re-applied on restore; the path is persisted, **not** the derived pin specs), `N` (pin → signal name; empty = NC; optional 4th field carries `nc_origin_tag()`: `U` = ImportedUnconnected, `D` = DroppedSingleton — omitted when the pin has a signal or when origin is `None`), `S` (signal → type override; only emitted for non-default), `C` (connection header with endpoints + `transform_name`), `W` (wire pair within the current connection). The 4th N field is backward-compatible: pre-existing snapshots without it restore with `nc_origin = None`. **Signals** carry a `type` (`SignalType::Power | GndShield | Other`). The `Signal` constructor **defaults to `Other`** — auto-inference no longer happens at construction. Types are set in three ways, in priority order: 1. **`infer_signal_types(System*)`** (`src/system/analysis.{hpp,cpp}`) runs at the end of every `load` (after `drop_singleton_signals`). It assigns: - `GndShield` when the **name alone** is unambiguous (`GND`, `SHIELD`, `CHASSIS`, `EARTH`, …) — false-positive rate is essentially zero on these. - `Power` requires (a) the name heuristic (`infer_signal_type` says Power), (b) a **hard fan-out floor**: signals with fewer than `POWER_FANOUT_HARD_FLOOR = 3` pins are *always* refused, regardless of name or voltage pattern (a real rail physically cannot land on just 1-2 pads), and (c) at least one positive structural signal — fan-out ≥ `POWER_FANOUT_CONFIRM_MIN = 4` **or** a voltage pattern in the name (`3V3`, `5V`, `12V`, …; detector: a `V` adjacent to a digit). This catches `VSEL_*`, `PWR_OK`, `_VDD_SENSE` etc. which look like Power by name but aren't real rails. Both thresholds are exposed in `analysis.hpp` so the analyze screen can render the same reasoning without duplicating constants. - `Other` otherwise. The "name-said-Power-but-refuted-by-structure" count is reported by `load`. 2. **`set-signal-type `** is the user override and wins over any inference. 3. **`restore`** reads `S` records, which only exist for non-default (`Other`) types — so save/restore round-trips both inferred and overridden types. The explore screen shows the type in the signal detail header. **Pin spec (expected attributes)**: every Pin carries a `PinSpec spec` (`src/system/pin_spec.hpp`) — the *expected* half of verification, set from a model: `function` (Power/Ground/Signal/Clock/NoConnect/Jtag*), `direction` (In/Out/Bidir/Passive/Power), `pad` (physical package terminal, e.g. a BSDL ball), and `source` (which model wrote it). `set-connector-type` populates it via `pin_role(connector_type, pin_name) → PinSpec`. `Pin::expected_signal_type()` is now a **derived accessor** — `to_signal_type(spec.function)` (Power→Power, Ground→GndShield, else Other) — not a stored field; the *observed* half stays `Pin::signal()` + the net + inference, and `verify` diffs the two. The VPX 3U connector lookup (`vpx_3u_role`) is still a stub returning Other, so connector-typed pins resolve to function Unknown until that table is filled. **`direction`/`function`/`pad` are populated from BSDL** via `attach-bsdl` (see below) and consumed by the model-driven checks (`check_pin_specs`: contention / undriven / NC-wired) and the JTAG chain check (`check_jtag_chain`), both run by `verify`. **Connector pin layout (preparation)**: `pin_layout(connector_type)` returns the canonical full pin-name list for a known connector kind, and `FillPartFromLayout(part, kind)` materialises NC pins for any layout position absent from the imported netlist. `set-connector-type` calls it after setting `connector_type` (no-op today since `pin_layout` is a stub returning `{}` for everything — populate alongside `vpx_3u_role`). End-to-end chain in place: `set-connector-type → FillPartFromLayout → pin_role`. **BSDL models (`attach-bsdl`)**: `attach-bsdl ` parses a BSDL device through `libbsdl` (wrapped by `BsdlModel`, `src/system/bsdl_model.{hpp,cpp}`), then `apply_bsdl(part, model)` binds each port to a Pin **by port name first, then by physical pad** — so a netlist that names IC pins either by signal or by package ball both bind. Each bound pin gets its `spec` set: `direction` (BSDL in/out/inout/linkage), `function` (TAP role → Jtag\*, `linkage` → Power/Ground/NoConnect by name, else Signal), `pad` (PIN_MAP ball), `source = Bsdl`. The `.bsd` path is stored on `Part::bsdl_path`, persisted via the `B` tag and re-applied on `restore`. Real-world check: an `xcku15p` FPGA in a VPX system binds 1517/1517 ports. **Unified application (`apply_model`)**: connector layout and BSDL are two implementations of one `PinModel` interface (`src/system/pin_model.{hpp,cpp}`: `spec_for(pin_name)`, `layout()`, `source()`). `ConnectorModel` wraps `pin_role`/`pin_layout`; `BsdlPinModel` wraps a parsed `BsdlModel`, indexed by both port name and physical pad. A single `apply_model(Part*, const PinModel&)` materialises the layout pins missing from the netlist, then sets each pin's `spec` **only where the model speaks** (`spec.source != None`) — so one source never clobbers another's. `set-connector-type` and `attach-bsdl` both funnel through it (the latter via the thin `apply_bsdl` adapter); `verify` stays agnostic of where a spec came from. A future SPICE/Modelica source would be a third `PinModel`. **`verify` (five passes)**: (1) typed pins — local mismatch between each pin's `expected_signal_type()` (derived from its `PinSpec`) and the actual signal type; (2) bridged nets — Power↔GndShield inconsistencies; (3) orphan summary `N orphan pin(s) at import (X imported NC, Y dropped singleton)` (filters out pins bridged via any `Connection::pin_map` — typically `FillIdentityNCs`-materialised); (4) **model-driven pin checks** (`check_pin_specs`): `DriveContention` (≥2 push-pull `Out` on a net), `UndrivenNet` (a **fully-modelled** net with input(s) but no driver — nets with any Unknown-direction pin are skipped, so un-modelled drivers don't cause false positives), `NcWired` (a no-connect pin on a multi-pin net); (5) **JTAG chain** (`check_jtag_chain`): collects TAP pins by `spec.function`, maps each to its net, emits `JtagTapIncomplete` / `JtagBusUnbridged` (TMS or TCK not common to all TAP devices) / `JtagChainBreak` (dangling TDO/TDI, chain fan-out, or not a single head→tail daisy chain). The BFS-reached `(module, signal)` set for any signal is shown live in `explore`'s detail pane when a signal entry is selected. **`analyze` (post-processing pass)**: `analyze_system(System*) → AnalysisReport` (`src/system/analysis.{hpp,cpp}`) is a stateless read-only pass that detects structural signal groups and anomalies. Per-module (signals are module-scoped): - **Diff pairs**: signal names ending `_P` / `_N` (case-insensitive) grouped by stem. Both halves present → candidate matched pair. - **Diff buses**: ≥ 2 matched diff pairs whose pair-stems share a common outer-stem after stripping a trailing integer (`MDI0` / `MDI1` / `MDI2` → outer `MDI` + indices). The strict `_` rule from plain buses does NOT apply to this trailing-index split: `_P`/`_N` was already stripped, so we know remaining digits are an index. Two index variants accepted: contiguous (`MDI0`) and underscore-separated (`PCIE_TX_0`). Emitted as `SignalGroup{kind=DiffBus, lo, hi}` with label `OUTER[lo..hi]_P/N`. Members include all 2·N constituent signals. A "bus" of size 1 falls back to `DiffPair` (single index does not a bus make). - **Buses**: two accepted forms — bracketed `NAME[N]` or strict-underscore `NAME_N`. The strict `_` rule before the digits is what avoids matching names like `GETH_01_VDD12` (no `_` before `12`). A stem with ≥ 2 entries becomes `SignalGroup{kind=Bus, lo, hi}`. - **Anomalies** detected: `DiffPairOrphan`, `DiffBusGap` (missing lane in `MDI[0..3]_P/N`), and `BusGap` (missing index inside a plain bus `[lo..hi]`). The diff-pair orphan reporter is **asymmetric on purpose**: only `_P` without `_N` is reported, because `_N` is overloaded with active-low semantics (`RESET_N`, `BOOTMODE_N`) and reporting both directions floods the output with false positives. - **Filters** to keep noise low: signals starting with `$` are skipped (Mentor's internal `$Nxxxx` net names). Exposed as the `analyze` shell command which prints groups (sorted by module + label) followed by anomalies. Designed to be consumed by the upcoming dashboard so the summary is visible at a glance. Tests: `tests/test_analysis.cpp`. **Component classification**: every `Part` carries a `ComponentKind kind` (`Passive | Semiconductor | IntegratedCircuit | Connector | TestPoint | Switch | Crystal | Mechanical | Other`) inferred at construction by `infer_component_kind(name)` from the leading reference-designator letter(s) (longest-match: `LED/TP/SW/FB/MK/MP/MH/HS/RA/RN/RP/RV` first, then single-letter R/C/L/F/D/Q/U/J/P/Y/X/S). Recomputed on `restore` (no persistence tag). Not yet exposed in TUI commands — branchpoints will be `set-connector-type` guard, `explore` filter, and `explore` header. `SignalType` lives in its own header `src/system/signal_type.hpp` (extracted from signals to avoid a pins↔signals include cycle). **Pins** are either NC (`signal() == nullptr`) or connected to exactly one signal. The ODS importer creates a Pin for every row that has a non-empty pin name, even when the signal column is empty or `"NC"` — the pin stays in the Part as NC. `restore` replaces `Tui::sys` entirely (`unique_ptr::reset`). Names are stored as-is — must not contain TAB or newline (true for the EE netlists we ingest). Format is versioned by the `# essim system snapshot v1` header for future compatibility. **NC origin tag**: each `Pin` carries `NcOrigin nc_origin` (`None | ImportedUnconnected | DroppedSingleton`, default `None`). Set in three places: (a) Mentor importer when the signal field starts with `unconnected` → `ImportedUnconnected`; (b) `drop_singleton_signals(Signals*)` called at the end of `load` → `DroppedSingleton` on each detached pin (signals with exactly one pin are NC by definition — see commits motivating this); (c) `duplicate` propagates the tag. Pins materialised by `FillIdentityNCs` keep `None` — they have no local signal but are bridged via `pin_map` and shouldn't be counted as orphans. The tag is persisted (see `N` record), reported as a total in `verify`, and tested in `tests/test_nc_origin.cpp`. **Connector types & transforms**: every `Part` carries a `connector_type` string (default `""`, set via the `set-connector-type` command — inline `set-connector-type m p kind` or bare which opens a TUI screen with module menu, part filter+menu, type input, list of types already in use, and an Apply button). When `connect` validates a pair, it consults `TransformRegistry::lookup(p1->connector_type, p2->connector_type)` (defined in `src/system/transform.{hpp,cpp}`) — both directions of the pair are tried. If neither is registered, an `IdentityTransform` fallback wires each pin of A to the canonical-equivalent pin of B (when present). The resulting `(Pin*, Pin*)` list and the transform's name are stored on the `Connection` (`pin_map`, `transform_name`). To register a real transform: define a `Transform` subclass in `transform.cpp` and call `TransformRegistry::get().add("kindA", "kindB", new MyTransform())` at init — there's no startup hook for this yet, so a small `RegisterBuiltinTransforms()` helper is the natural place to add when more types appear. **Identity wiring uses canonical names**: `IdentityTransform::apply` builds `unordered_map` for side B and looks up each side-A pin by its canonical form. So `A1` (one card) auto-pairs with `A001` (the other) thanks to `canonical_pin_name` (`pre + zero-padded(3) digit suffix`; mixed/non-numeric returns the original). Same canonicalisation in `CheckIdentityCompatible`. **`pin_role` doesn't need canonicalisation** because `parse_pin` extracts `(col, row)` via `stoi` which already strips leading zeros. **Subset wiring + NC backfill**: `CheckIdentityCompatible(a, b, info=&s)` accepts the case where one side's canonical pin set is a subset of the other's — typical when one importer drops NC pins (Altium) and the other doesn't (Mentor). It populates `info` with a non-fatal "N pin(s) only on ''" message. Bidirectional mismatch (both sides have orphans) is still refused. After acceptance, `connect` calls `FillIdentityNCs(p1, p2)` which materialises the orphan canonical positions on the missing side as NC pins (`new Pin(other_side_name)`) — so `Connection::pin_map.size()` matches the larger side's count. Idempotent. `screen_idx` mapping: **4 = dashboard (home, set in the constructor)**, 0 = console (textual shell + log view), 1 = connect, 2 = set-connector-type, 3 = explore, 5 = analyze. The dashboard is the boot screen; the console is the secondary screen reachable via the `[c]` shortcut and used to display textual output from `verify`/`analyze`/etc. plus collect arguments for multi-step commands. The label was renamed from "log" to "console" because the screen is also where commands are typed — "log" only described half of what it does. **The previous `search` and `net` screens (formerly `screen_idx = 1` and `5`) have been removed** — `explore` is a superset of both. Search by pattern: use `explore`'s child filter. Net tracing: select a signal in `explore` and the detail pane lists both the local pins and the BFS-reached `(module, signal)` members across connections. The palette (`Ctrl-P`) also covers the "jump to this signal / module" use case. **Dashboard letter conflicts**: with the screen renames, `[c]` now opens the **console** rather than `connect`. The connect command is surfaced as **`[p]lug`** on the dashboard (a UI rename only — the canonical command stays `connect` for script + save/restore stability, with `plug` registered as an alias so the palette finds it under either name). **Esc navigation**: every non-home screen returns to the dashboard. The dashboard itself swallows Esc (no parent) — use `[q]uit` or the `quit` command via the palette / shell to leave. From the shell, Esc cancels a pending multi-step prompt if one is in flight, otherwise returns to the dashboard. **Quit propagation**: the `quit` / `exit` commands set `Tui::quit = true` *and* call `screen_ptr->Exit()` directly, so quitting works from any screen (including via the palette while sitting on the dashboard). The legacy `if (quit) screen.Exit();` in the shell Renderer stays as a belt-and-braces backup. Adding a new screen mode is the same recipe each time: state members, `Container::Vertical` of focusable components, a `Renderer` lambda that recomputes derived state per frame (e.g. filtered part lists), an entry in `Container::Tab`, and Tab/Esc handling in the outer `CatchEvent`. **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. - **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`). Everything is recomputed every frame so manual overrides via the signal-type popup are reflected immediately. Esc returns to the dashboard. The dashboard's previous `[v]erify` letter shortcut was removed — its content is fully covered by this screen. The textual `verify` / `analyze` commands still exist for scripts. **Command group factorisation**: `RegisterCommands()` in `commands.cpp` owns most built-ins, but self-contained groups live in their own files (one `RegisterCommands()` member each). Today only `RegisterExportCommands()` in `commands_export.cpp` follows the pattern. Adding a new group is mechanical: declare a new member in `tui.hpp`'s `private:` section, define it in `commands_.cpp`, and call it from the orchestrator. Each group can have file-local helpers (e.g. `commands_export.cpp` has its own anonymous-namespace `csv_quote` and `pin_side`). **Generic file-picker dialog** (`screen_filedialog.cpp`): one reusable modal for every "pick a path" interaction. State lives in `Tui::file_dialog` (a single `FileDialogState`); attached to the tab tree via `Modal(BuildFileDialog(), &file_dialog.open)` in `Run()`. API: - `OpenFileDialog(title, persist_key, default_filename, filters, on_confirm)` — opens the modal, restoring the last-used `(dir, filename)` for `persist_key` if previously saved. - `ConfirmFileDialog()` runs on Enter on the OK button: validates against the filter whitelist (rejects unknown extensions with an in-dialog status message), persists `(dir, filename)` under `persist_key`, closes the modal, then calls `on_confirm(full_path)`. The optional `filters` vector (`{label, extension}` pairs) renders a horizontal Toggle at the top of the dialog. The first frame after Open seeds the filter index from the filename's current extension; subsequent index changes rewrite the filename extension so the caller's extension-based dispatch picks the right format. Empty filter list ⇒ no Toggle shown, no extension validation. Today the only caller is `export` (`{"CSV", ".csv"}, {"ODS", ".ods"}` filters, key `export.connections`), but future callers (`save`, `restore`, `source`, …) plug in by changing four arguments. **Generic error modal** (`screen_error.cpp`, `Tui::ShowError(msg)`): centred `borderRounded` popup with red title, the message (wrapped via `paragraph`), and an OK button. Esc / Enter dismiss. Stacked at the top of the Modal chain in `Run()` so it overlays every screen and every other modal. The error is also `Print()`-ed to the console log, so it remains inspectable after dismissal. Used by the export action (unknown extension, open-for-write failure, ODS save failure, unknown kind); other actions can adopt by replacing user-visible `Print("...failed...")` calls with `ShowError(...)`. **Per-key path persistence** (`SaveLastUsed(key, dir, filename)` / `LoadLastUsed(key, &dir, &filename)` in `shell.cpp`): each key writes a tiny two-line file (`dir\nfilename\n`) under `UserDataDir() / .last`. `UserDataDir()` is the cross-platform `XDG_DATA_HOME` / `LOCALAPPDATA` etc. helper also used by the command history file. Free functions, not Tui members, so any module (the file dialog today; could be the script-save buffer or the save command tomorrow) can use them with the same minimal API. **ODS writer** (`src/imports/ods_writer.{hpp,cpp}`): minimal OpenDocument Spreadsheet writer. Backed by libzip + pugixml (both already pulled in by the ODS importer). Class hierarchy is two structs: - `OdsSheet` — sparse row-major grid of string cells (`set(row, col, value)`). - `OdsWriter` — owns the sheets, emits a valid `.ods` archive with `mimetype` (stored uncompressed, magic header), `META-INF/manifest.xml`, and `content.xml`. `content.xml` is built with pugixml: one `` per sheet, each row a `` of string `` cells. String-only by design (no numbers, dates, formulas, styles, merges) — the format minimum for "rectangular text data" is concise enough to live in ~200 lines without depending on a heavyweight ODS library. **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. **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. **Focus highlighting**: each interactive screen reuses `FocusLabel(elem, focused)` from `tui_helpers.hpp` (inline helper, `focused ? e | inverted : e`) on the label of the currently-focused field so the user sees at a glance where the next keystroke lands. Indices match the `Container::Vertical` order — e.g. `connect` has 7 (m1, p1-filter, p1-menu, m2, p2-filter, p2-menu, Button), `explore` has 6 (module, type, child-filter, child, detail-filter, detail). Buttons (`Connect`, `Apply`) get the highlight on the button itself, not a separate label. **Context help panel**: every screen renders a right-column help panel via `RenderHelpPanel(title, entries)` (`tui_helpers.{hpp,cpp}`). Fixed width 30 cols, key column 9 chars, then a flex description. The entries list is per-screen so it acts as a context cheat-sheet: dashboard advertises its letter shortcuts + `Ctrl-P` + `q`; the shell advertises history / scroll / `Ctrl-P` / common commands; the interactive screens advertise `Tab` / `↑↓` / `Enter` / `Esc` semantics. Helps replace the previous one-line dim footers, which were truncated at small widths. **Main-screen title bar**: top of `BuildMainScreen` renders `" essim "` (bold) + `"— system digital twin"` (dim) on the left and a live `"N module(s), M connection(s)"` (or `"no system loaded"`) on the right, then a `separator()`. Built from `sys->modules()->size()` / `sys->connections()->size()` each frame — cheap. `screen_main.cpp` therefore needs to include `system/system.hpp`, `system/modules.hpp`, `system/connect.hpp` directly (forward-declared in `tui.hpp`). **Main-screen scrollback**: the visualisation area lets you scroll through past output. State is `Tui::scroll_offset` (0 = follow tail). Keys (default branch of the outer `CatchEvent` in `Run()`): `PageUp` / `PageDown` step 10 lines, `Home` jumps to top, `End` returns to tail. `Print()` resets `scroll_offset = 0` so any new output snaps the view back to the tail (otherwise late errors would be hidden). Render: instead of `focusPositionRelative(0, 1)` always anchoring to the bottom, the screen places `| focus` on `output[size - 1 - scroll_offset]` and shows a `[scroll: -N / PgUp PgDn Home End]` indicator next to the prompt when offset > 0. `vscroll_indicator | yframe` for the FTXUI-side scroll bar. `connect` is dual-mode (`prompt_for_missing = false`): - Inline: `connect m1 p1 m2 p2`. Each part argument is resolved as exact name first, then case-insensitive substring; ambiguous → lists candidates and aborts. - Bare: opens a dedicated full-screen layout (`screen_idx = 2`) with two columns (`endpoint 1` / `endpoint 2`). Each column has a module `Menu`, a part filter `Input`, and a filtered part `Menu`. A `Button(" Connect ")` at the bottom commits. `Tab` cycles focus across the 7 components, `Esc` leaves. The connect screen rebuilds the filtered part lists inside the `Renderer` lambda each frame (cheap; lets the part menus react live to module changes and filter typing). Selection indices are clamped if a list shrinks below the previous index. The Connection record stores `Module*`/`Part*` for both endpoints (added to `Connection` for this — minimal struct fields, no behaviour change). **Signal-type popup (shared)**: Enter on a signal entry in the `explore` screen opens a modal (`Tui::sigtype_dialog_open`) that lets the user pick `power | gnd | other` for the currently-selected signal. Built in `screen_sigtype_modal.cpp::BuildSignalTypeModal()`, attached to both screens via the `Modal(...)` decorator in `Run()`. Inside the modal, Enter applies + closes + records `set-signal-type ` in the script-save buffer (`recorded`); Esc closes without applying. Two safeguards: (a) re-selecting the type the pin already has is a no-op and records nothing; (b) the recorder collapses consecutive edits of the same `(module, signal)` — if the previous line in `recorded` already targets the same pair, it is replaced rather than appended. The outer `CatchEvent` in `Run()` cedes Tab/Esc to the modal whenever `sigtype_dialog_open` is true, so the underlying screen doesn't yank focus back. In `explore` the popup also fires from the detail pane when browsing `parts`: each `pin → signal` row carries its signal name in the parallel `explore_detail_sig` vector, and Enter on a non-`(NC)` row opens the popup for that signal. **Net tracing in `explore`**: when the user selects a signal entry in `explore` (type tab = `signals`), the detail pane shows two sections: the local pins of that signal (module/part/pin labels), then — if `find_net(sys, module, signal)` returns ≥ 2 members — a `Net members (across connections)` section listing every `(module, signal, type)` reachable through `Connection::pin_map`. The signal-detail header includes `K net members across N module(s)` (or `INCONSISTENT` if `net_type_consistent` returns false). This is what replaced the former dedicated `net` screen. **Enter-to-edit matrix in `explore`** — `explore_detail_sig` stores a `module\tsignal` payload per detail row (empty = read-only). The detail-menu `on_enter` parses the `\t` and calls `OpenSignalTypeDialog(mod, sig)`, so Enter works correctly for peer-module rows. Today: | Focus | parts | signals | connections | |--------------------|-----------------------|----------------------------------|-------------| | `children_menu` | jump to set-connector-type screen pre-filled on this part (focus on type input) | open signal-type popup for the selected signal | — | | `detail_menu` | open signal-type popup for the pin's signal (skipped on `(NC)`) | open signal-type popup for the row's `(module, signal)` — works for both local pins (= current signal) and cross-module net members | — | Module / type / filter focuses do not have an Enter binding. **Long-running scripts (`source`)**: `Source(file)` is event-paced. It reads all lines, sets `loading = true`, then spawns a detached pacing thread that posts `Event::Special("\x02tick")` every ~30 ms — **but only one tick at a time**: the main thread acks each one (`tick_in_flight = false` at the end of `ProcessNextSourceLine`) before the ticker sleeps and posts the next. Without this, FTXUI batches all queued events into one render pass; a long line (e.g. a heavy Mentor parse) would let the ticker queue many ticks and the modal would freeze. Each tick triggers `ProcessNextSourceLine`, which processes one effective line (skipping comments/blanks) via `Submit()`. A centred `borderDouble` modal (`" Computing… "` + filename + `N / M lines`) is overlaid via `dbox` while `loading` is true. Adding a new screen mode = drop a `screen_.cpp` defining `Component Tui::BuildXxxScreen()`, add corresponding state members in `tui.hpp`, register a `BuildXxxScreen` declaration in the screen-builders block, then in `tui.cpp::Run()` add a child to `Container::Tab` and a case in the screen-mode `switch` of the outer `CatchEvent` (Tab cycling + Esc-leave). Commands lived in `commands.cpp` set `screen_idx` to enter the new mode. Command history is persisted on disk and loaded on startup. Path resolution is platform-aware: - Linux/macOS: `$XDG_DATA_HOME/essim/history`, falling back to `~/.local/share/essim/history`. - Windows: `%LOCALAPPDATA%\essim\history`, falling back to `%APPDATA%\…` then `%USERPROFILE%\AppData\Local\…`. Each successful submission appends a single line to the file (so a crash doesn't lose history). Multi-step prompt answers are NOT persisted — only top-level commands. ## Categorization rules (normative) Everything in this section is a precise description of how signals, pins, parts, and analysis groups are classified. Each rule lists its input, its check, its output, and the source file. Code and doc are kept in sync — when a threshold changes, update both. ### Signal type (Power / GndShield / Other) Default: `Signal::type = SignalType::Other` (constructor does no inference). Type is set by `infer_signal_types(System*)` (`src/system/analysis.cpp`), called at the end of every `load` and after `duplicate`. The decision per signal: 1. Compute `named = infer_signal_type(name)` (`src/system/signals.cpp`): - `GndShield` if name matches **case-insensitively**: `GND`, `GROUND`, `EARTH`, `SHIELD`, `CHASSIS`, or starts with `GND_` / `GROUND_` / `EARTH_` / `SHIELD_` / `CHASSIS_`. - `Power` if the name contains any of `PWR`, `POWER`, `VCC`, `VDD`, `VEE`, `VSS`, `VBAT`, or starts with `VS_`, `VS3_`, `+`, `-` followed by a digit. - Else `Other`. 2. If `named == GndShield` → final = `GndShield`. (Single criterion: name. False positives near zero.) 3. If `named == Power`: - **Hard floor**: fan-out `< POWER_FANOUT_HARD_FLOOR` (= **3**) → final = `Other`. Always overrides. A net touching only 1–2 pads physically cannot be a power rail. - **Else**: confirmation requires at least one of: fan-out `≥ POWER_FANOUT_CONFIRM_MIN` (= **4**), **or** a voltage pattern in the name (any `V` adjacent to a digit, case-insensitive — catches `3V3`, `5V`, `12V`, `0V9`). If either holds → final = `Power`. Otherwise → `Other`. 4. If `named == Other` → final = `Other`. `set-signal-type ` always wins. `save`/`restore` round-trips the final `type` via `S` records (only non-`Other` are persisted). ### NC pin origin `Pin::nc_origin` (`src/system/pins.hpp`). Default `NcOrigin::None`. Set by: - **`NcOrigin::ImportedUnconnected`** — Mentor importer (`src/imports/import_mentor.cpp`): the signal field of an `Explicit Pin:` row starts case-insensitively with `unconnected` (e.g. `'unconnected'`, `'unconnected (by TERM)'`). The pin is kept on the part with no signal. - **`NcOrigin::DroppedSingleton`** — `drop_singleton_signals(Signals*)` (`src/system/signals.cpp`) called at the end of `load`: any signal whose pin set has size exactly 1 is unconnected by definition. The pin is detached (`sig = nullptr`) and tagged; the `Signal` object is deleted. - **`NcOrigin::None` (no tag)** — pins materialised by `FillIdentityNCs` at `connect` time. These are unconnected locally but bridged to a real signal on the peer module via `Connection::pin_map`; they are explicitly excluded from the "orphan" count in `verify` and the analyze screen. ### Signal groups `analyze_system(System*)` (`src/system/analysis.cpp`) emits `SignalGroup`s **per module** (signals are module-scoped). Internal names (starting with `$` — Mentor's `$Nxxxx` convention) are skipped wholesale at every step. **DiffPair** (`GroupKind::DiffPair`): - Signal name ends `_P` or `_N` (case-insensitive). The character before the suffix must be `_`. - Group by stem (name minus `_P`/`_N`). Both halves present → emit `DiffPair` with two members. **DiffBus** (`GroupKind::DiffBus`): - Take every matched `DiffPair` candidate and split its stem with `split_trailing_index`: trailing digit run becomes the index, the rest the outer-stem. Two forms accepted (`MDI0` → outer `MDI` + 0; `PCIE_TX_0` → outer `PCIE_TX_` + 0). The strict `_` rule of plain buses does **not** apply here (the `_P`/`_N` was already stripped, so any trailing digit is an index). - Group by outer-stem. **≥ 2** pairs → `DiffBus` with label `OUTER[lo..hi]_P/N` and 2·N members. Exactly 1 → falls back to `DiffPair` (a single index does not make a bus). **Bus** (`GroupKind::Bus`): - Two accepted forms: bracketed `NAME[N]` (where `N` is digits between `[` and `]`), and underscore `NAME_N` (where `_` is **strictly required** between the stem and the trailing digit run — this is what rejects `GETH_01_VDD12` from being parsed as a bus). - Group by stem. **≥ 2** signals → `Bus` with label `STEM[lo..hi]`. ### Analysis anomalies `analyze_system` reports the following: - **`DiffPairOrphan`** — a stem has `_P` but no matching `_N`. The reverse (`_N` orphan) is **deliberately not reported** because `_N` is overloaded with active-low semantics (`RESET_N`, `BOOTMODE_N`, `PRESENT_N`); flagging it would flood the output with false positives. - **`BusGap`** — within a `Bus[lo..hi]`, one or more indices in the interior are missing. - **`DiffBusGap`** — within a `DiffBus[lo..hi]`, one or more lane indices in the interior are missing. The analyze screen additionally surfaces two "verify-class" issues, computed the same way as the textual `verify` command: - **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`). They consume the BSDL-populated `PinSpec` plus `compute_all_nets`. Surfacing them in the analyze/dashboard Issues pane is a TODO. ### Component kind `Part::kind` is inferred at construction (`src/system/component_kind.cpp`) from the leading reference-designator letter(s) of the part name. **Longest-match wins**: - Two-letter prefixes (checked first, case-insensitive): `LED → Semiconductor`, `TP → TestPoint`, `SW → Switch`, `FB → Passive`, `MK / MP / MH → Mechanical`, `HS → Mechanical`, `RA / RN / RP / RV → Passive`. - Single-letter fallback: `R / C / L / F → Passive`, `D / Q → Semiconductor`, `U → IntegratedCircuit`, `J / P → Connector`, `Y / X → Crystal`, `S → Switch`. - No match → `Other`. Recomputed on `restore` (no persistence tag). Currently not used by any decision flow — branch points are search filter / `set-connector-type` guard / explore header. ### Connector wiring (transforms) `connect` looks up a registered transform for `(p1->connector_type, p2->connector_type)` via `TransformRegistry::lookup`, tried in both directions. Fall-through is `IdentityTransform`: - Compares pin sets by **canonical name** (`canonical_pin_name(s)`, `src/system/pin_name.cpp`): split into prefix + digit suffix; if the suffix is pure digits, zero-pad to width 3 (`A1` → `A001`, `A001` → `A001`, `A1B` → `A1B`, `VCC` → `VCC`). - `CheckIdentityCompatible(a, b)` accepts the **subset case** (one side's canonical set is a subset of the other's — typical because Altium drops NC, Mentor doesn't). Bidirectional mismatch (both sides have orphans) is refused. - After acceptance, `FillIdentityNCs(p1, p2)` **materialises** the missing canonical positions on the smaller side as new NC pins (`new Pin(other_side_name)`, no signal attached, `nc_origin = None`). Idempotent. Pins materialised this way are bridged via `Connection::pin_map` to a real signal on the peer module — the orphan counter in `verify` and the analyze screen excludes them. ## Gotchas - All three importers (`IMPORT_MENTOR`, `IMPORT_ALTIUM`, `IMPORT_ODS`) are wired in `System::Load`. Wrap calls in `try/catch` (the TUI does). - **Altium importer drops NC pins entirely**: the source format only enumerates pins inside `(signal …)` blocks, so positions not connected to any signal on this card never become `Pin`s. Mentor (via `Explicit Pin:`) and ODS (one row per pin) materialise NC. This is the asymmetry that motivates `FillIdentityNCs` at `connect` time and (eventually) `FillPartFromLayout` at `set-connector-type` time. - **Mentor importer + NC**: the Mentor `.qcv` format names every pin's signal explicitly. Sentinel values like `'unconnected'` or `'unconnected (by TERM)'` mean NC — the parser detects them via `is_nc_signal_name` (lowercase prefix match) and keeps the pin on the part with no signal, tagged `ImportedUnconnected`. Additionally, after each `load` the system runs `drop_singleton_signals(mod->signals)`: any signal whose pin set has size 1 is unconnected by definition (electrically nowhere to go), so it is detached and the lone pin is tagged `DroppedSingleton`. The count is shown inline in the `load` output. The semantics covers both Mentor patterns and the few `NC_*`-prefixed signals that turn out to be singletons in real-world boards — the name `NC_*` alone is *not* enough (most of them connect two or more parts and are real bridges, even if cosmetically called NC). - ODS importer: each spreadsheet sheet becomes a `Part` (sheet name = part name). Rows are pin/signal pairs; the **first non-empty row of each sheet is dropped as a header** (no validation of header content). Empty cells skip the row; `"NC"` keeps the pin in the part but doesn't connect it to a signal. Pins or parts whose name collides (rare in well-formed sheets) are silently dropped. - `System::Load` throws `std::runtime_error("Unknown import type")` for any value outside the three enum cases. - `Modules`/`Parts`/etc. have no const-correct iteration on the `*` accessor; iterators on `SystemElementContainer` are available but `begin()`/`end()` are non-const-safe in some places. ## Documentation `doc/` hosts: - `api/` — auto-generated Markdown C++ API reference (developer-facing). - `user/` — hand-written intro + auto-generated command reference (user-facing). - `Doxyfile.in` — CMake-templated Doxygen config (XML-only output to `build/doc/xml/`). - `gen_api_md.py` — custom XML → Markdown emitter (~330 lines, stdlib-only Python). - `README.md`, `classes.puml` — meta + diagram. **API reference pipeline**: `doxygen` extracts the XML from `src/**`, then `gen_api_md.py` walks `index.xml` plus each `class*.xml` / `*_8hpp.xml` and emits one Markdown file per class/struct/file plus an index. Source-code references use relative paths (`../../../../src/...#L42`) so gitea's renderer turns them into clickable line-anchored links. Chosen over `doxybook2` (not in Arch/AUR) and `moxygen` (Node dep) to keep the toolchain pure-C++/Python and trivially modifiable. **Command reference pipeline**: `essim --commands-md ` instantiates a `Tui` (which calls `RegisterCommands()`), then `Tui::DumpCommandsMd(ostream&)` iterates the `commands` map and writes Markdown — two groups (interactive / other), each command with description, numbered arguments, completion mode per arg, scriptability/interactivity notes. The binary is the single source of truth, so a new `CommandSpec` field is reflected in the doc as soon as `DumpCommandsMd` is taught to render it (no separate parser to drift). The `doc` target `DEPENDS essim` so a stale binary is rebuilt automatically. `doc/user/index.md` and `doc/user/scripting.md` are hand-written (tutorial + scripting semantics); only `doc/user/commands.md` is regenerated. Regenerate everything with `cmake --build build --target doc` — the target is auto-created when both Doxygen and Python 3 are present at configure time; otherwise the regular build target is unaffected and a status line records which tool is missing. The Markdown output is committed (`doc/api/`, `doc/user/commands.md`) so gitea readers see fresh docs without building. ## Memory layout for sessions Persistent notes live in `~/.claude/projects/-home-francois-Projets-essim/memory/`. Index: `MEMORY.md`. Add project/feedback/user/reference entries there when relevant — see top-level Claude Code memory rules. This file is `DESIGN.md` (renamed from the original `CLAUDE.md`).