dwarves-gui/src/lib.rs
//! **dwarves-gui (library)** — the demo's modules exposed as a LIBRARY so the whole
//! shipped shell is reachable from the `tests/robot_scene.rs` integration suite. A
//! binary-only crate exposes NOTHING to an integration test (there is no linkable lib
//! target), so EVERY module lives here and the binary (`main.rs`) is a thin host that
//! consumes this one definition — there is no lib/bin TWIN (L5).
//!
//! **The twin is dead.** `main.rs` used to re-declare the same module files itself, so
//! they compiled TWICE into two crates and the two `build_native_app`s DRIFTED (the lib
//! copy had lost the fat/embedded funnel default and 9 tabs, invisibly to the compiler).
//! `main.rs` now declares NO modules: it calls [`build_native_app`] from here. Moving a
//! module means the old copy is GONE.
//!
//! The tab roster is [`DWARVES_TABS`] — the "no tab dropped" ORACLE (korp's proven
//! `KORP_TABS` shape): [`build_native_app`] takes its labels from this const and the
//! scene/menubar/robot suite derive from the same list, so a dropped tab is a compile /
//! test failure, never a silently shorter shell.
// The ⚒ Build-Thing pane folds a genuinely large `serde_json::json!` object (the whole
// roster + launch cells + boot + matrix + registry + the error CODES). `json!` expands
// one recursion level per field, so the default 128 is not enough — raise it here rather
// than splintering one coherent state object into artificial sub-builders.
#![recursion_limit = "512"]
pub mod panel;
// The native tabbed shell + its panes (the tabbed app is `cfg(not wasm)`; the wasm web
// demo mounts the single Forge panel, so only `panel` compiles on wasm).
#[cfg(not(target_arch = "wasm32"))]
pub mod tabbed;
// WHICH PHASE a populate is in (clone → warehouse-build → scan-lens), published by
// dwarves into the SHARED `step-board` registry korp's collectors publish into — so the
// Re-sync bar says which of the three is running instead of filling up when the clone
// ends and sitting there through the other two.
#[cfg(not(target_arch = "wasm32"))]
// **The generation watch** — the ONE mechanism every pane uses to decide when to
// re-read. SCAN is not a user action (owner, 2026-08-22 and again 2026-08-25), so a
// pane reads itself when the thing it reads MOVES, never when somebody presses a
// button. Same key idea as `nornir::lens_chain::generation`, for the sources that are
// not lens tables (a file, a ledger, a checkout) — one helper rather than a hand-rolled
// poll loop per pane (LAW 5).
pub mod generation;
pub mod populate_steps;
// The warehouse SELECTOR (nornir parity: many registered, ONE selected — the funnel +
// release bind to it) and the About popup identity (shared facett-about card).
#[cfg(not(target_arch = "wasm32"))]
pub mod about;
#[cfg(not(target_arch = "wasm32"))]
pub mod warehouse_select;
// The unified **Jera** central panel (shared `facett_zoneui::JeraCentral`) — replaces
// the old Jobs / Jobs (live) duality with ONE jobs/zones panel.
#[cfg(not(target_arch = "wasm32"))]
pub mod jera;
// The 🧬 **Dupes** tab — a `Mimir.SimilarCode` SUB-VIEW. Since the lens chain landed the
// duplicate answer is computed by a jera job and READ off `lens_similar_code` over the
// SAME thin `Mimir.SimilarCode` RPC the Lenses pane's SimilarCode view uses — this pane
// does NOT fold (LAW 5: the old local `detect()` over `Warehouse.Scan` was a twin of the
// lens and is gone from the findings path). It keeps an APPLY escape hatch that folds
// LOCALLY on demand, because `lens_similar_code` carries ranked clusters, not the
// `dwarves::dupes` executable RefactorPlans the apply seam drives.
#[cfg(not(target_arch = "wasm32"))]
pub mod dupes;
// The 📐 **Coverage** tab — the COVERAGE INDEX. dwarves' answer to "is this actually
// finished?": which functions no test can reach, which tests can never go red, and —
// the half L3 demands that nothing measured before — which HOT PATHS carry no bench.
// Read off nornir's warehouse (`symbol_facts` / `call_edges` / `test_inventory` /
// `bench_runs` / `bench_results` / `test_outcomes`) over the SAME thin gRPC read seam
// the Binary / Coderoom / Dupes tabs use — never a re-scan of source (L5), never a
// `nornir` crate edge. All logic lives in the `dwarves::coverage` engine (which
// consumes autonom's `Gap = Surface - Covered - Allowlist` gate rather than inventing
// a fourth coverage model); this module is the read seam + the render. Bench rows are
// partitioned by HOST and never merged, and an empty warehouse table renders as a
// MISSING FEED rather than as "no gaps".
#[cfg(not(target_arch = "wasm32"))]
pub mod coverage;
// The 🔍 **Lenses** tab — the three MIMIR LENSES. Cross-fact queries over nornir's
// warehouse that nothing else can answer: the monomorphisation bloat tax (what a generic
// costs in SHIPPED BYTES, from `symbol_sizes` x `binary_facts`), zombie cargo features
// (flags gating code nothing reaches, and flags no build variant ever turns on), and
// change-risk scoring (Adam Tornhill's published behavioural model — cited on screen, not
// claimed as ours). One pane with three views, not three tabs: all three are the same
// shape (a ranked list of findings with its evidence beside it) and three near-identical
// renderers would be a twin (L5). All logic lives in the `dwarves::mimir` engine; this
// module is the read seam + the render. Its fourth view renders `mimir::PROPOSED_RPCS` —
// the L5a note that the QUERY half of each lens belongs in nornir — on screen, so the
// staging decision is visible rather than buried in a module doc.
#[cfg(not(target_arch = "wasm32"))]
pub mod mimir_lens;
// The 🏢 **Data view** tab — the warehouse DASHBOARD, back under Mímir. Mounts the SHARED
// `facett_warehousedeck::WarehouseDeck` (a FacetDeck WALL of N panes, each binding one
// Mímir/warehouse table to a view-kind: 3D graph / grid / chart) over the selected
// workspace, fed Arrow frames from the SAME thin `Warehouse.Scan` gRPC read seam the
// Binary / Coverage / Lenses tabs use — plus a DataFusion SQL prompt (the engine skade
// re-exports) that runs over the fetched frames and renders the result as a grid + a
// deck pane. Native-only (warehouse-fed over gRPC), like the other warehouse tabs.
#[cfg(not(target_arch = "wasm32"))]
pub mod dataview;
// The 🌊 **Codeflow** tab — the call graph as LIVING FLOW on the shared
// `facett_flow::FlowGraph` (the owner, 2026-10-18: "yes codeflow"): nodes and
// lines from `Viz.CallGraph`, each line's flow rate from the latest traced
// run's measured call counts (static call sites stand in when no run exists,
// and the pane names which). Native-only, like the other warehouse tabs.
#[cfg(not(target_arch = "wasm32"))]
pub mod codeflow;
// The 🌐 **Flow3D** tab — codeflow's 3D sibling: the SAME call graph + traced
// rates on the shared `facett_graph3d::Graph3D` cloud (force layout, orbit +
// depth cues), the rates driving PULSES along curved 3D arcs (the viewer's
// `rate` feature). Native-only, like the other warehouse tabs.
#[cfg(not(target_arch = "wasm32"))]
pub mod flow3d;
// The 📄 **graph source pane** — the CONSUMER of a graph-node click. `Graph3D` already
// resolves a pointer press to a node index; the deck that mounts it had no typed way to
// hand that index back, so every warehouse-deck graph answered the click and dropped the
// answer. This module reads the answer, resolves the label to a definition span through
// `scip_occurrences` (exact `enc_start_line`/`enc_end_line`) with a `symbol_facts`
// fallback, and paints the source in a RESIZABLE right-hand panel beside the wall. A node
// with no known definition SAYS SO — a repo node resolves to that repo's manifest, never
// to an invented function. Native-only, like the Data view it sits in.
#[cfg(not(target_arch = "wasm32"))]
pub mod graph_source;
// The 🌐 **Warehouse Navigator's REAL function tier** — the live thin-fetch that turns
// the world/universe graph's function nodes from SYNTHETIC into REAL warehouse symbols.
// A `RealFnSource` (the navigator's injection seam) that reads `symbol_facts` +
// `call_edges` over the SAME thin `Warehouse.Scan` gRPC read seam the Data view / Lenses
// tabs use (`nornir_warehouse_embed::scan_warehouse_table`) and groups them into per-crate
// `CrateFuncGraph`s via the shared `nornir_depgraph::funcgraph` model. Native-only, like
// the other warehouse-fed tabs; the wasm web demo keeps the fixture-only landing.
#[cfg(not(target_arch = "wasm32"))]
pub mod warehouse_nav;
// The ⚒ **Build Thing** tab — a DATA-DRIVEN scene that knows no product by name.
//
// `build_scene` is the surface: type an artifact name (or point at a recipe by
// coordinate), get that recipe's home matrix, pick ONE row and ONE column, press BYGG,
// and a pane appears below whose shape is the `PaneKind` the recipe's `ArtifactKind`
// resolves to — argv+stdout, start/stop+log+ports, or run/reboot+serial|graphical. The
// whole switch is three arms and no `if` over a name; `tests/robot_build_scene.rs`
// measures that by tokenising the scene's own source.
//
// `build_forge` is the other side of the seam — the part that is allowed to know what a
// product is. It translates every side-by-side checkout's `.nornir/build-variants.toml`
// into that matrix WITHOUT changing it (rows in declaration order, suite badges copied
// verbatim, a column exactly when a declaration produced one) and hands the verbs to the
// engines that already exist: `dwarves::build_thing::BuildRun` for a build, `boot_run`'s
// `BootRun` for a container log or a guest console.
/// The verbs a typ bears, as data (gap #7): buildinfo always, RUN / BOOT / UNPACK by
/// typ, the `where` val, the run surfaces — the local twin of edda's `actions(kind)`.
pub mod actions;
#[cfg(not(target_arch = "wasm32"))]
pub mod build_forge;
/// The build GRAPH ⚒ draws: the menu and the trees lowered to the one DAG renderer —
/// variants and features as nodes, containment and consumption as edges.
pub mod build_graph;
#[cfg(not(target_arch = "wasm32"))]
pub mod build_scene;
/// The step console (wave 3, gap #4): the edge DAG a `BYGG <kant>` plans — donor first,
/// consumer last — each step's state in five words, the honest expectation before the
/// run, the CLI line it equals, and the receipt beside the build's and the RUN's.
pub mod build_steps;
/// ⚒ Build Thing as a facett-SCENE — five draggable leaves instead of one Slot, the
/// fifth being the kconfig configurator. ⚠ The tree is DECLARED and not yet mounted;
/// see the module docs for what is real today and what is design.
pub mod build_thing_scene;
/// The settings TREE ⚒ draws in its settings leaf: feature ▸ setting ▸ sub-setting,
/// the `kärna` node with the kconfig canvas as its detail, every node with its path
/// and the prompt flag it equals (law C).
pub mod settings_tree;
// The **kernel feature scene** — no longer a tab of its own (the ⚙ Kernel tab was
// retired 2026-08-26); it is the ⚒ Build Thing tab's `build-thing-kernel` LEAF, hosted
// through `Forge::leaf` (a picked kernel cell) and `Forge::browse` (nothing picked).
// The preset a `[[build.kernel]]` row declares, drawn
// as a slowly rotating 3D graph of its own kconfig symbols, with the cascade a click
// produces: kept · locked · clicked-off · cascade-dropped (amber) · refused (red).
//
// It is the THIRD consumer of one camera (`facett_graphview::camera3d::project_azimuth`,
// after the funnel's 3D Plan-DAG and the warehouse world graph) and it borrows the 3D
// node-state vocabulary (`facett_graph3d::NodeState`) rather than spelling a fourth one.
// Everything it draws is derived from the committed kconfig fragments in edda's
// `nornir-build-thing`: no solver, no container, no shell. What it CANNOT derive it
// says out loud — see the module docs on isolated nodes and the invisible `defconfig`
// floor. What a click says BEYOND the name hierarchy — what leaves the `.config` by
// Kconfig's own edges — is the kconfig solver's `kernel.why.toml` that a BUILD left
// beside the kernel (`kernel_scene::KconfigWhy`): read here, never run here.
// Native-only, like the tabbed shell.
#[cfg(not(target_arch = "wasm32"))]
pub mod kernel_scene;
/// **Syscall profiles** — the Heimdall tuner's review step: a thin layer on the
/// `dwarves tune` verbs, run as queued jera jobs.
#[cfg(not(target_arch = "wasm32"))]
pub mod tune_pane;
// The ▶ **Build & Boot** seam — clicking Build on a `kind=vm` launch row (korp-kvm)
// builds the appliance PID-1 and boots it under QEMU/OVMF/KVM through tunnr's boot
// primitive, streaming the live serial console into the pane. See [`boot_run`].
pub mod boot_run;
// The 🔑 **Deploy key** tab — the SSH identity the forge fetches with. Eight repos
// failed every 60-second poll for most of 2026-07-22 with "needs a deploy key, but
// none was found" and the UI said NOTHING; the only channel was journalctl. This tab
// shows the public key with a copy button and where to paste it (per-repo deploy key
// vs account key), the real per-remote fetch state from a live SSH handshake with the
// actual error, and WHICH rung of the resolution ladder answered. Its five states —
// no key found · minted-not-registered · present-but-never-used · rejected · fetching
// — are five, and "nothing measured" is never "fine". Native only: it resolves a key
// off the filesystem and opens sockets.
#[cfg(not(target_arch = "wasm32"))]
pub mod deploy_key;
#[cfg(not(target_arch = "wasm32"))]
pub mod pipeline;
/// The Pipeline tab's runs leg: the selected pipeline's real runs, read from the forge.
#[cfg(not(target_arch = "wasm32"))]
pub mod pipeline_runs;
#[cfg(not(target_arch = "wasm32"))]
pub mod status;
// The **Release** tab bound to the SELECTED workspace (its convergence file), the
// common **Settings** pane (workspace · fat/thin · theme · vault-unlock), and the
// **Profile** view (shared Identity + vault credential NAMES).
// The ONE graph lowering + measured graph pane both DAG surfaces (release
// convergence, funnel) go through — the shared vello-backed `facett-graphview`
// engine, with no node ceiling and a per-frame cost the pane reports.
#[cfg(not(target_arch = "wasm32"))]
pub mod dag_graph;
// 🌳 Tree — the front page of 🏠 Workspace: the workspace's build/read graph with live
// marks, folded by the FORGE (`Dwarves.Tree`) and drawn on the one graph renderer.
#[cfg(not(target_arch = "wasm32"))]
pub mod front_tree;
// 🧭 Explorer — the first tab of ⚒ Build: the Dwarves project as a tree (project → Mímir
// → workspaces → Build Things → pipelines) and each node's detail, asked of the forge
// (`dwarf.v1.Explorer`); its buttons queue jera jobs on the forge and follow them.
#[cfg(not(target_arch = "wasm32"))]
pub mod explorer;
#[cfg(not(target_arch = "wasm32"))]
pub mod release;
// The 🚀 **Mímir release** tab (2026-10-15): the release plan of the SELECTED workspace
// as Mímir sees it — order, dependencies, convergence, what is affected since the last
// release — asked of the forge (`dwarf.v1.MimirRelease.Plan`, computed on its jera
// queue). Next to `release`, which stays as it is; it reads no checkout on this machine.
#[cfg(not(target_arch = "wasm32"))]
pub mod mimir_release;
// The **Testmatrix + Benches** view (task #66) bound to the SELECTED workspace: the
// reused `facett_matrix::MatrixView` testmatrix grid + the `nornir_bench`-read
// `nornir.mimir.<populate|search>` bench trend (where a human watches epic #62 improve).
#[cfg(not(target_arch = "wasm32"))]
pub mod benches;
// The **Fleet** coverage board (autotest North Star): MOUNTS the shared
// `facett_fleet_view::FleetView` — a full-system-autotest run as a member×surface grid
// under a `Gap == ∅` verdict banner. Fed a `FleetRunView` (nornir-testmatrix GateReport +
// TestResultRows + members) offline from `$DWARVES_FLEET_RUN`, else the never-blank demo.
#[cfg(not(target_arch = "wasm32"))]
pub mod fleet;
// The **Binary** tab: the shared `facett_binview::BinaryPane` over the binary half of the
// SELECTED workspace's code room (Mímir v2) — `Viz.Binary` asked through the forge
// (`CodeRoom.Answer`, computed on the server's jera `rooms` lane): bytes by crate, the
// largest symbols, the size per binary release. No pre-v2 table is read.
#[cfg(not(target_arch = "wasm32"))]
pub mod binview;
// The **Coderoom** tab (task #92): the shared `facett_coderoom_view::CoderoomPane` fed with
// REAL rows — dwarves' `DwarvesCoderoomData` reads the coderoom-warehouse tables
// (symbol_facts crates@versions / semver_diffs compat edges / call_edges references) that
// `nornir coderoom {ingest,compat}` writes into the DEDICATED coderoom warehouse, over the
// SAME `dyn Warehouse` gRPC read seam the Binary tab uses (`nornir_warehouse_embed::
// scan_warehouse_table`), pointed at the coderoom warehouse (NOT the selected workspace Mimir).
#[cfg(not(target_arch = "wasm32"))]
pub mod coderoom;
// The 🏛 3-D mode of the Coderoom tab — the Mímir graph as a map you fly over: repos and
// crates far, function cards (head, params / locals under it, call sites, splines to the
// callees' heads) near, a head's source on hover (`Viz.CodeMap`, through the forge).
pub mod coderoom3d;
pub mod trace_replay;
/// The code room as a pane sees it: the ONE seam every code-graph pane asks nornir's
/// room through (`CodeRoom.Answer` on the forge) and the callout it leads with (the
/// answer's state, its evidence, the room it was read from).
#[cfg(not(target_arch = "wasm32"))]
pub mod room;
// The **Saga** tab (TASK #42): the shared `dwarves_embed::SagaView` tape-recorder —
// list a workspace's recorded sessions (painful-first) + scrub/play/step the
// selected one via the reused `nornir_saga::ui::Player`, re-rendering the app's
// state over time. Fed the `.jsonl` tape `dwarves saga import` lands (file seam).
/// 📖 The Manual tab — dwarves' `.nornir/*.md` doc set embedded in the binary (behind
/// `manual-embed`) and rendered in the SHARED `facett_manual::ManualPane`.
pub mod manual;
#[cfg(not(target_arch = "wasm32"))]
pub mod profile;
#[cfg(not(target_arch = "wasm32"))]
pub mod saga;
#[cfg(not(target_arch = "wasm32"))]
pub mod settings;
// The Profile tab's HOST sign-in seam (behind the `login` feature): dwarves'
// `DwarvesLoginSeam` implements the shared `facett_auth_ui::LoginSeam` over the
// `nornir-auth-client` device-code grant, sealing the token in the encrypted vault via
// `VaultTokenStore`. OFF ⇒ the Profile pane runs the honest `facett_auth_ui::StubLogin`.
#[cfg(all(not(target_arch = "wasm32"), feature = "login"))]
pub mod login_seam;
// The wire-remote robot-drive MOUNT (task: North-Star autotest last mile). Behind
// `robot-remote` it exposes `serve_robot_remote(<socket>)` — a `cmd_json::CmdJsonServer`
// over the egui shell on a Unix socket the fleet drives. Native-only + feature-gated so
// the shipped default binary never mounts a socket.
#[cfg(all(not(target_arch = "wasm32"), feature = "robot-remote"))]
pub mod robot_remote;
// The per-tab robot SCENARIO LIBRARY (North-Star follow-up): a recorded `Scenario` per
// viz tab (select → interact → assert-over-`state_json`), exposed as consumable wire
// DATA so the fleet's `RobotDriveMember` drives SPECIFIC dwarves tabs, not just a ping.
// Shared by BOTH robot modes: the headless wire-remote mount AND the visible watch replay
// drive these SAME recorded scenarios.
#[cfg(all(
not(target_arch = "wasm32"),
any(feature = "robot-remote", feature = "watch")
))]
pub mod scenarios;
// The ONE `Observable` app + `draw(ui, app, targets)` closure both robot modes drive, so
// the headless mount and the visible watch window exercise the SAME shell identically.
#[cfg(all(
not(target_arch = "wasm32"),
any(feature = "robot-remote", feature = "watch")
))]
pub mod robot_shell;
// The ONE `--robot-mode` decision (headless vs watch vs refuse). Gated with the pair
// above, not with `watch`: the headless drive needs only `robot_shell` + `scenarios` +
// robotui's non-optional `egui_kittest`, so it must be reachable in a build that has no
// windowing stack at all. Before this module existed, `--robot-mode headless` fell past
// the watch-only parser and OPENED A WINDOW.
#[cfg(all(
not(target_arch = "wasm32"),
any(feature = "robot-remote", feature = "watch")
))]
pub mod robot_mode;
// The VISIBLE robot-watch replay (feature `watch`): `--robot-watch` opens a real eframe
// window and replays the recorded tab tour so a human WATCHES the robot navigate every
// tab, through the shared `nornir_robotui::run_scenario_mode(..., RunMode::Watch, ...)`.
// Native-only + feature-gated so the shipped default binary never links the eframe/winit
// windowing stack the watch backend pulls.
#[cfg(all(not(target_arch = "wasm32"), feature = "watch"))]
pub mod robot_watch;
/// A single process-wide lock serializing every test that mutates the shared
/// `DWARVES_VAULT_*` / `HOME` / `DWARVES_OIDC_*` process-global env (cargo runs tests in
/// parallel threads sharing ONE environment). Shared across ALL dwarves-gui test modules
/// (journey / profile / settings / login_seam) — never a per-module twin (L5) — so a
/// vault/login/settings/journey test can't stomp another's session or HOME mid-run.
/// Mirrors dwarves' crate-level `ENV_LOCK`. **Ungated behind `#[cfg(test)]`**: the
/// premise that "only the `vault`/`login` tests mutate that env" was false — Settings'
/// `robot_finds_the_claude_token_control_in_settings` (settings.rs) removes
/// `ANTHROPIC_API_KEY`/`ANTHROPIC_AUTH_TOKEN` and runs in the DEFAULT build, so the
/// feature gate made `cargo test -p dwarves-gui` fail to COMPILE with no features
/// (`error[E0425]: cannot find value ENV_LOCK in the crate root`). It is never dead
/// code: at least one default-build test takes it.
#[cfg(test)]
pub(crate) static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
// ── the shipped tab ROSTER — the "no tab dropped" oracle ──────────────────────
// korp's proven `scene_shell::KORP_TABS` shape (a `(slot-id / key, title)` const the
// shell, the scene and the robot suite all derive from), applied to dwarves.
/// The canonical dwarves tab roster as `(slot-id / key, label)`, in the shipped order.
/// The key is `tabbed::slug(label)` — the SAME id `tabbed::dwarves_scene` gives each
/// `Slot` pane and the SAME key `TabbedView::state_json` folds each pane's state under.
/// `slug` is why `"CI/CD"` keys as `"ci-cd"`: a `/` is a path separator in the deep-link
/// `app#pane` form, so no label can leak one into an addressable id (asserted below).
///
/// This is the single list [`build_native_app`] hands to `TabbedView` (it ZIPS the
/// constructed panels against these labels, so a panel added without a roster entry —
/// or a roster entry with no panel — fails the length oracle), the list
/// [`tab_labels`] feeds `tabbed::dwarves_scene`, and the list the
/// `tests/robot_scene.rs` robot suite drives. There is no second hardcoded tab list
/// anywhere: "no tab dropped" is an oracle the compiler and the tests enforce.
#[cfg(not(target_arch = "wasm32"))]
pub const DWARVES_TABS: &[(&str, &str)] = &[
// In AREA order — see [`DWARVES_AREAS`]; the first tab is the landing view.
// 🏠 Workspace — the tree is the front page (the landing view).
("tree", "Tree"),
("status", "Status"),
// 🧭 Code = Mímir. Dupes is GONE from the top menu (owner 2026-08-22: "IGEN
// dublett, skall inte ligga i top menyn") — it is the Similar Code SUB-VIEW under
// Lenses.
("coderoom", "Coderoom"),
("lenses", "Lenses"),
("data-view", "Data view"),
("warehouse", "Warehouse"),
("codeflow", "Codeflow"),
("flow3d", "Flow3D"),
// ⚒ Build = CI/CD. ⚒ Build Thing's body is FIVE leaves — recipes | matrix |
// settings | kernel | console — so the kconfig configurator that used to be the
// ⚙ Kernel tab (retired 2026-08-26, the owner) is a leaf of the tab where the
// build is chosen.
("explorer", "Explorer"),
("pipeline", "Pipeline"),
("ci-cd", "CI/CD"),
("builds", "Builds"),
("build-thing", "Build Thing"),
("syscall-profiles", "Syscall profiles"),
("forge", "Forge"),
("jera", "Jera"),
// 🧪 Test & quality
("benches", "Benches"),
("coverage", "Coverage"),
("binary", "Binary"),
("fleet", "Fleet"),
("saga", "Saga"),
// 🚀 Release
("release", "Release"),
("mimir-release", "Mímir release"),
// 🛠 Work & settings
("funnel", "Funnel"),
("claude", "Claude"),
("settings", "Settings"),
("profile", "Profile"),
("deploy-key", "Deploy key"),
("manual", "Manual"),
];
/// **The menu in six AREAS** (owner 2026-10-15: "dont drop anyone"; CI/CD is Build).
/// The rail shows these six; inside an area its tabs sit in one row. Every
/// [`DWARVES_TABS`] key is in exactly one area, in roster order — asserted by
/// `roster_tests` — so no tab is dropped and none is shown twice.
#[cfg(not(target_arch = "wasm32"))]
pub const DWARVES_AREAS: &[(&str, &[&str])] = &[
("🏠 Workspace", &["tree", "status"]),
("🧭 Code", &["coderoom", "lenses", "data-view", "warehouse", "codeflow", "flow3d"]),
(
"⚒ Build",
&[
"explorer",
"pipeline",
"ci-cd",
"builds",
"build-thing",
"syscall-profiles",
"forge",
"jera",
],
),
(
"🧪 Test & quality",
&["benches", "coverage", "binary", "fleet", "saga"],
),
("🚀 Release", &["release", "mimir-release"]),
(
"🛠 Work & settings",
&[
"funnel",
"claude",
"settings",
"profile",
"deploy-key",
"manual",
],
),
];
/// The shipped tab LABELS, in the shipped order — [`DWARVES_TABS`]'s titles. This is
/// what `tabbed::dwarves_scene` consumes, so the scene, the menubar and the robot
/// suite all describe the SAME 18 panes the shell mounts.
#[cfg(not(target_arch = "wasm32"))]
pub fn tab_labels() -> Vec<String> {
DWARVES_TABS
.iter()
.map(|(_, label)| (*label).to_string())
.collect()
}
/// The shipped tab slot KEYS, in the shipped order — [`DWARVES_TABS`]'s ids (each label
/// slugified). The keys a robot addresses panes / folds / deep-links by.
#[cfg(not(target_arch = "wasm32"))]
pub fn tab_keys() -> Vec<String> {
DWARVES_TABS
.iter()
.map(|(key, _)| (*key).to_string())
.collect()
}
// ── native host ──────────────────────────────────────────────────────────────
// The desktop window is a small TABBED app. The Forge tab is the LIVE `dwarves-embed`
// forge; the Funnel tab is the LIVE nornir funnel; the Jera tab is the unified shared
// `facett_zoneui::JeraCentral` (jobs + zone column).
/// This crate's build stamp — `v<version> (<git hash>)`, WITHOUT a product name.
///
/// The version is `CARGO_PKG_VERSION`; `DWARVES_GIT_HASH` is emitted by `build.rs` (a
/// pure-fs git read, zero-shell). Callers name the artifact themselves through
/// [`version_line_for`]; this half exists so the two cannot drift.
pub fn build_stamp() -> String {
format!(
"v{} ({})",
env!("CARGO_PKG_VERSION"),
env!("DWARVES_GIT_HASH")
)
}
/// **The build stamp, prefixed by the artifact that is printing it** —
/// `<variant> v<version> (<git hash>)`.
///
/// # The defect this closes, MEASURED on t14s 2026-08-27
///
/// `/usr/local/bin/dwarves-ui` and `/usr/local/bin/dwarves-ui-gpu` were **byte-identical**
/// — both sha256 `6eb9ec98717b270e…`, both equal to `~/cargo-target/release/dwarves-ui-gpu`,
/// with no `dwarves-ui` in that target tree at all. ONE artifact under TWO names, and
/// `--version` could not reveal it: this function used to be
///
/// ```text
/// dwarves v0.1.4 (c9dc909)
/// ```
///
/// for `dwarves-ui`, `dwarves-ui-gpu`, `dwarves-solo` and every other `[[bin]]` off this
/// crate's ONE `src/main.rs`. Three declared variants, one string. An operator holding two
/// files could ask each of them what it was and be told the same thing twice.
///
/// # Why the caller passes the name instead of this function deciding
///
/// Because the ONLY value that can answer truthfully is `env!("CARGO_BIN_NAME")` expanded
/// **in the binary crate**, and a library cannot see it — in a library `CARGO_BIN_NAME` is
/// unset.
///
/// It must not be a feature test. `cfg!(feature = "gpu")` here would answer the SAME for
/// both binaries whenever cargo builds them in one invocation, because feature resolution
/// is per-PACKAGE, not per-target: `dwarves-ui-gpu`'s `required-features = ["gpu"]` turns
/// `gpu` on for the package, and the plain `dwarves-ui` compiled in that same invocation
/// sees it too. Proven on korp 2026-08-27 with `cfg!(feature = "map3d-gpu")`, which
/// answered "gpu" for BOTH binaries. `CARGO_BIN_NAME` is per-TARGET and survives it.
///
/// So the rule is: the BINARY names itself, from the one string cargo guarantees is its
/// own, and this function only formats.
pub fn version_line_for(variant: &str) -> String {
format!("{variant} {}", build_stamp())
}
/// **Does the artifact named `variant` own the GPU renderer lane?**
///
/// `true` iff the name ends in `-gpu`. The rule lives here, as a `const fn`, so it is
/// asserted by a test that needs neither binary built — and so `main.rs` can use it in a
/// `const`.
///
/// # Why a NAME and not `cfg!(feature = "gpu")`
///
/// MEASURED on oden 2026-08-27, Xvfb :96, a real window, both bins out of ONE
/// `cargo build -p dwarves-gui --bin dwarves-ui --bin dwarves-ui-gpu --features gpu`:
/// the plain `dwarves-ui` (sha256 `186d2caf084a93e8…`) printed
///
/// ```text
/// dwarves-ui-gpu: instanced cloud renderer installed (UNIVERSE star-field on the GPU)
/// ```
///
/// It linked the renderer, ARMED it, and named itself after its twin. Cargo resolves
/// features per PACKAGE, so `dwarves-ui-gpu`'s `required-features = ["gpu"]` turns `gpu`
/// on for `dwarves-ui` as well whenever the two are built together — and the fleet's own
/// build door does exactly that. `#[cfg(feature = "gpu")]` therefore cannot mean "this
/// artifact is the GPU one".
///
/// `CARGO_BIN_NAME` is per-TARGET and survives unification, which is the same reason the
/// crash report and the `--version` stamp read from it (see [`version_line_for`]).
pub const fn gpu_lane_armed_for(variant: &str) -> bool {
// `str::ends_with` is not const; compare the tail bytes directly.
const SUFFIX: &[u8] = b"-gpu";
let v = variant.as_bytes();
if v.len() < SUFFIX.len() {
return false;
}
let off = v.len() - SUFFIX.len();
let mut i = 0;
while i < SUFFIX.len() {
if v[off + i] != SUFFIX[i] {
return false;
}
i += 1;
}
true
}
/// Backwards-compatible spelling of [`version_line_for`] for the `dwarves-ui` artifact.
///
/// Kept because the About box and the wasm host have no `CARGO_BIN_NAME` to pass; the
/// native `main` uses [`version_line_for`] with its own, which is the whole point.
pub fn version_line() -> String {
version_line_for("dwarves-ui")
}
/// The shipped native shell title (kept in one place so `main` + the robot suite agree).
/// Names every [`DWARVES_TABS`] label — asserted by `roster_tests`.
#[cfg(not(target_arch = "wasm32"))]
pub const NATIVE_TITLE: &str = "dwarves — Tree | Status | Coderoom | Lenses | Data view | Warehouse | Codeflow | Flow3D | Explorer | Pipeline | CI/CD | Builds | Build Thing | Syscall profiles | Forge | Jera | Benches | Coverage | Binary | Fleet | Saga | Release | Mímir release | Funnel | Claude | Settings | Profile | Deploy key | Manual";
/// The registered warehouses (nornir parity), from the REAL registry. Queries the
/// running nornir-server's `Workspaces.List` (`registry.redb`) over the SAME gRPC seam
/// the Add/Populate form uses — never a reinvented registry read. The `fallback` names
/// (env / `nordisk`) are unioned in and are the whole result when the server is offline
/// or serves nothing, so the picker is never empty and never blocks the UI on a dead
/// server. Order: fallback first (stable), then any additional served names.
#[cfg(not(target_arch = "wasm32"))]
pub fn registered_warehouses(endpoint: &str, token: &str, fallback: Vec<String>) -> Vec<String> {
let mut names = fallback;
if let Ok(served) = nornir_warehouse_embed::list_warehouses(endpoint, token) {
for w in served {
if !w.name.is_empty() && !names.iter().any(|n| n == &w.name) {
names.push(w.name);
}
}
}
names
}
/// Assemble the native tabbed shell — the ONE `TabbedView` the binary runs AND the robot
/// suite drives, so both see the EXACT shipped set of [`DWARVES_TABS`] tabs (there is no
/// second builder: the lib/bin twin that had silently lost the fat/embedded default and 9
/// tabs is deleted). `jera_ledger` is the jera `JobRecord` ledger root the unified Jera
/// panel reads live (empty ⇒ its zoned fixture fallback). Offline-safe: dead endpoints
/// render each pane's honest unavailable state rather than hanging on a socket.
#[cfg(not(target_arch = "wasm32"))]
pub fn build_native_app(
forge_endpoint: String,
jera_ledger: std::path::PathBuf,
) -> tabbed::TabbedView {
use facett_core::Panel;
// ── HOST-ONLY: the UI holds NO nornir address — it reaches nornir THROUGH the host. ──
// The suite-host-embedded-auth LAW: nornir is EMBEDDED in the dwarves server; the
// UI defers to the HOST and dials ONLY `forge_endpoint` (`:7900`). It must NEVER open
// a socket to nornir (`:7878`) — that port is for EXTERNAL nornir consumers only.
//
// The warehouse / Mímir data these panes render is served THROUGH the host: the dwarves
// forge now re-serves the `nornir.v1` READ surface (`Warehouse.{Tables,Scan}` /
// `Workspaces.*` / the eleven `Mimir.*` lens folds) on its OWN `:7900` socket, forwarding
// each call to its embedded nornir (`crate::warehouse_proxy`, host-only follow-up). So the
// wiring seam below is the FORGE endpoint — the panes' `nornir-warehouse-embed` thin client
// speaks the same `/nornir.v1.Warehouse/Scan …` paths, but to the dwarves host, which
// proxies. The `:7878` socket never enters the UI. A dead/embedded-nornir-less host makes
// a proxied read return an honest Unavailable, so the panes still degrade HONESTLY rather
// than fabricate — they just never dial nornir directly.
//
// Served-via-host panes (now LIVE through the proxy):
// Warehouse navigator · Data view · Dupes · Coverage · Lenses (Mímir) · Binary · Coderoom
// · the warehouse SELECTOR's registry list + Add/Populate.
// The Funnel is embedded-FIRST with a host-injected live read leg over this same
// endpoint (consulted lazily; honest named outage when the host does not serve
// `/nornir.v1.Funnel/Show`) — see below.
let warehouse_endpoint = forge_endpoint.clone();
let endpoint = warehouse_endpoint;
// ── the warehouse SELECTOR (nornir parity) ──────────────────────────────────
// Populate the registry (many warehouses) and pick the ONE active. Options come
// from `$DWARVES_WAREHOUSES` (comma-separated) or default to the constellation's
// `nordisk` warehouse; the initial selection is `$NORNIR_WORKSPACE` if it is a
// registered option, else the first. ALL warehouse-bound tabs bind to this selection;
// changing it re-fetches for the newly-selected warehouse.
// ── HOST-ONLY bearer, RESOLVED THROUGH THE DWARVES CHAIN ────────────────────
//
// The warehouse/Mímir panes + the registry list/Add/Populate talk to the dwarves
// HOST (which proxies to its embedded nornir), so they carry the DWARVES host
// bearer — the SAME credential the Forge/Builds/Jera panes use. Still NOT the
// `$NORNIR_SERVER_TOKEN`/`~/.nornir/token` ladder: the UI holds no nornir address,
// so it holds no nornir credential.
//
// ⚠ This line was `std::env::var("DWARVES_SERVER_TOKEN").unwrap_or_default()` — a
// hand-rolled ONE-RUNG copy of a ladder that already existed. The `dwarves` CLI has
// always resolved its bearer through
// `nornir_service::resolve_token(--token, $DWARVES_SERVER_TOKEN, ~/.dwarves/token)`;
// the client had the env rung and nothing else, so a UI started with
// `--server <url>` and no exported variable sent an EMPTY bearer while
// `~/.dwarves/token` sat there holding the secret, and the pane's own message
// asserted that no file fallback existed. The owner, 2026-08-27: "the token chain is
// wrong EVERY time server and client are started."
//
// `dwarves_embed::ClientCredential` is now the ONE client resolver, and it reports
// WHICH rung answered — which is what makes "no credential configured" and "token
// rejected" different sentences with different repairs. Empty is still legal (the
// host's dev default is an open server, and the panes' gRPC tests drive a mock that
// wants none), so an unresolved credential is reported, never refused here. The
// embedded Funnel needs no token (it dials nothing).
let credential = dwarves_embed::ClientCredential::resolve();
let token = credential.token.clone();
// Printed beside the endpoint line, because a bearer that resolved from a file the
// operator forgot about is exactly as surprising as one that did not resolve at
// all. Never the secret — the rung and the length.
eprintln!("dwarves-gui: bearer ← {}", credential.provenance());
// The picker OPTIONS come from the REAL registry (nornir parity): query the running
// nornir-server's `Workspaces.List` (the same `registry.redb` `workspace_<name>` set
// the nornir CLI's `workspace ls` reads) via `nornir-warehouse-embed::list_warehouses`
// — no reinvented registry, the SAME gRPC seam the Add/Populate form uses. Offline /
// unreachable server ⇒ fall back to `$DWARVES_WAREHOUSES` (or `nordisk`) so the shell
// is never empty. The env-provided names are unioned in so a not-yet-registered target
// still appears.
// No hardcoded "nordisk": the picker reflects the REAL registry (empty after an
// uninstall/purge — the box must not resurrect a workspace the user nuked). Only
// an explicit `$DWARVES_WAREHOUSES` seeds names; otherwise the served registry is
// the whole truth (empty ⇒ the shell opens on its honest "no workspace" state).
let env_warehouses: Vec<String> = std::env::var("DWARVES_WAREHOUSES")
.ok()
.map(|s| {
s.split(',')
.map(|w| w.trim().to_string())
.filter(|w| !w.is_empty())
.collect()
})
.filter(|v: &Vec<String>| !v.is_empty())
.unwrap_or_default();
let warehouses = registered_warehouses(&endpoint, &token, env_warehouses);
let initial = std::env::var("NORNIR_WORKSPACE")
.ok()
.filter(|w| warehouses.iter().any(|x| x == w));
let initial = initial.unwrap_or_else(|| warehouses.first().cloned().unwrap_or_default());
// The selector, with the "Add warehouse (SSH) → Populate" affordance wired to the
// nornir-server endpoint + token: `➕ Add…` registers a warehouse from an SSH git
// remote (nornir clones + builds it), and Populate re-syncs — the UI lives here, the
// warehouse-WRITE happens in nornir over `Workspaces.Register`/`Fetch` (no cycle).
let selector = warehouse_select::WarehouseSelector::new(warehouses.clone(), initial.clone())
.with_add_source(endpoint.clone(), token.clone());
// `$DWARVES_ROBOT_POPULATE_STEPS` — seed the populate step board to a named posture,
// THROUGH the real producer (`populate_steps::observe`), so a robot screenshot of the
// bar shows something that went down the same code path a live poll does. ROBOT
// builds only (`cargo robot-ui`): a shipped `dwarves-ui` does not contain the
// postures, so no environment variable can put invented populate progress on a
// user's screen.
#[cfg(any(
test,
feature = "fixtures",
feature = "robot-remote",
feature = "watch"
))]
populate_steps::apply_robot_posture();
// The LANDING view: the Warehouse Navigator (dwarves' first page). A 2D
// constellation→repo→crate dependency navigator over the SHARED nornir-depgraph
// suite model + the reused facett-graphpan renderer — click-to-drill-down,
// per-repo grouping hulls, categorical colour-by-repo (the validated dataviz
// palette), recency=glow. The repo/crate skeleton builds LIVE from the real member
// checkouts when a suite root is discoverable (`warehouse_nav::live_skeleton` —
// `WorkspaceGraph::build_from_members`; provenance in `skeleton_source`), falling
// back to the faithful nordisk fixture otherwise (offline-safe, immediate, honestly
// labeled either way); its WORLD / UNIVERSE function tier is wired LIVE
// to the selected warehouse over the SAME thin `Warehouse.Scan` seam the Data view /
// Lenses tabs use — `symbol_facts` + `call_edges` grouped into per-crate function
// graphs (`warehouse_nav::WarehouseNavSource`), pulled ONCE on first world build. A
// dead server keeps the honest synthetic fallback, so this is safe when nornir is down.
let warehouse: Box<dyn Panel> = warehouse_nav::warehouse_nav_pane(&endpoint, &token, &initial);
// The **Forge** — ONE live pane (unified: there is no longer a separate static
// "Forge" fixture tab + a "Forge (live)" tab — that duality confused). It fetches
// the run history + the selected run's DAG over thin gRPC from the RUNNING
// `dwarves-server` (default `http://127.0.0.1:7900`, override `$DWARVES_SERVER`),
// auto-polls, and shows real pipelines/runs/the DAG. Reachable-but-idle ⇒ an
// honest "connected · no runs yet"; truly offline ⇒ "forge unavailable". (The
// static `ForgeView` fixture survives only as the wasm web-demo panel, where there
// is no gRPC — see `start()`.)
let forge: Box<dyn Panel> = dwarves_embed::panel(forge_endpoint.clone());
// The build-pipeline DAG: the forge's build pipeline on the REUSED
// `facett-pipeline` viewer, fed by dwarves' `ForgePipelineAdapter` (steps →
// nodes, `needs` → edges, live ledger → status, build durations → labels).
// Mounted as a `SplitPanel`: `pipeline-main` (the reused facett-pipeline stage DAG) │
// `pipeline-inspector` (the source selector + stage detail, whose header used to wrap
// and eat the graph's height).
// ONE job source for every pane that shows jobs — the 🧬 Jera tab below and this
// pipeline pane's run-level light. The Pipeline tab used to derive its own
// `jobs.redb` path out of the UI's `$HOME`, which `9e82007` had already taken away
// from the Jera tab; two panes reading "the ledger" by two roads is how one of them
// stays quietly wrong. `crate::jera::job_source` owns the choice now.
let jobs_source = jera::job_source(
endpoint.clone(),
token.clone(),
initial.clone(),
jera_ledger.clone(),
);
let pipeline: Box<dyn tabbed::SplitPanel> =
Box::new(pipeline::pipeline_view_with_jobs(jobs_source.clone()).with_forge(forge_endpoint.clone(), token.clone()));
// The **CI/CD** wizard — the human-mode quick-setup pane: pick BUILD·TEST·DEPLOY·
// TRIGGER (+ an SSH source), GENERATE a typed dwarves_runa::Pipeline, and see the LIVE stage
// DAG on the REUSED facett-pipeline viewer (lit via RunaPipelineAdapter). A Run
// dispatches jera jobs over a fake scheduler (no real deploy). Same generator +
// runner as the `dwarves pipeline` CLI (one code path, two front doors).
let cicd: Box<dyn Panel> = dwarves_embed::cicd_wizard_panel();
// The **Builds** — the holger build-MATRIX pane (separation-spec B2). It resolves
// holger's 3 deployment tiers (embedded·mini·full) over thin gRPC from the RUNNING
// `dwarves-server`'s `ResolveHolgerBuild` RPC (COMPILE-FREE — a plan/wiring check, not
// a build) and renders the matrix (tier × target × status) on the REUSED facett-table.
// Honest states: no-build-space / unreachable / auth-failed / not-served — never blank.
let builds: Box<dyn Panel> = dwarves_embed::holger_build_panel(forge_endpoint);
// The ⚒ **Build Thing** — the pane that actually BUILDS. Where `Builds` above is a
// compile-free plan/wiring RESOLVE over gRPC, this one SELECTS a declared variant
// anywhere in the constellation and RUNS it: `nornir_build_thing::build_variant_observed`
// on a named worker thread, live progress streamed back, and a concrete result
// (artifact path + size, exit status, elapsed, compiler diagnostics verbatim, zero-egress
// verdict). holger's three mutually-exclusive tiers are the worked example; dwarves
// building ITSELF is the dogfood. Its second view is the FEATURE-axis coverage gap.
// Reads the side-by-side checkouts under `$DWARVES_CONSTELLATION` (else the parent of
// the repo root); an absent constellation renders as a named missing root, never as an
// empty build space.
//
// Mounted as a `LeafPanel`: its body DECOMPOSES into the FIVE named leaves
// `build_thing_scene::body()` declares — `build-thing-recipes` | `-matrix` |
// `-settings` | `-kernel` | `-console` — over four draggable dividers. That tree was
// written, documented and unit-tested long before anything could host it; the shell
// mounted the tab `Whole`, so `body()` had no callers and the module read as the
// running layout while describing a shape nothing rendered.
let build_thing: Box<dyn tabbed::LeafPanel> =
Box::new(build_forge::build_scene_pane().hosted());
// **Syscall profiles** — the review of a row's seccomp profiles (Heimdall tuner):
// the `dwarves tune` verbs as queued jera jobs, the verb's JSON drawn as it came.
let syscall_profiles: Box<dyn Panel> = Box::new(tune_pane::TunePane::live());
// ── the ⚙ Kernel tab is RETIRED (owner, 2026-08-26) ────────────────────────
//
// It drew the preset a `[[build.kernel]]` row declares as a rotating 3D graph of
// kconfig symbols. That facet has not gone anywhere — it is the ⚒ tab's `-kernel`
// LEAF, hosted through `Forge::leaf` / `Forge::browse`, and the preset picked there
// is the preset the build runs (measured: `nuked` → `full` moves `base()`
// tinyconfig → defconfig and changes `content_id()`).
//
// The comment that stood here kept the tab on ONE argument, and it was a real one:
// *"the ⚒ leaf only exists while a kernel row is picked, while this tab answers
// 'what is in a `slim` kernel' with nothing selected at all."* Retiring the tab
// without answering that would have dropped a capability silently. So it was
// answered first: with no selection the ⚒ kernel leaf asks the forge for
// `Forge::browse` — the SAME `KernelScene` instance, wearing its own preset strip
// because no host control is pointing at it — and the standing question has a home
// in the tab where the build is chosen. `robot_kernel_scene.rs` boxes the pane
// directly and is untouched by the tab going away.
//
// One control on one value survives: the two states are mutually exclusive per
// frame, so there is never a frame in which two widgets carry the same accessible
// name — which is exactly what two panes both titled "Warehouse" cost this shell.
// The 🔑 **Deploy key** — the SSH identity the forge fetches with, made self-service.
// Eight repos failed every 60s poll for most of 2026-07-22 with "needs a deploy key,
// but none was found" while the UI said nothing at all. This pane shows the PUBLIC key
// with a copy button and exactly where to paste it (per-repo deploy key — safer —
// vs. an account key), probes every discovered SSH remote for real on ONE named worker
// (serial: Codeberg rate-limits parallel handshakes with a reset that is not an auth
// failure), and names WHICH rung of the resolution ladder answered. Five states, never
// collapsed; "never probed" is its own colour and is not green.
let deploy_key: Box<dyn Panel> = Box::new(deploy_key::deploy_key_pane());
// The Funnel — EMBEDDED-FIRST with the HOST-injected LIVE READ leg (host-only
// doctrine, kept): the pane still runs the funnel IN-PROCESS (immediate, offline-
// safe, nothing dialed at construction — never a socket to nornir `:7878`, and the
// UI still holds no nornir address). But the host now injects a `LiveFunnelSource`
// over ITS one endpoint — the SAME forge endpoint + `$DWARVES_SERVER_TOKEN` bearer
// every served-via-host pane uses — consulted LAZILY on each poll: a host serving
// `/nornir.v1.Funnel/Show` makes the tab render the REAL `funnel_events`-backed
// funnel (an MCP-submitted idea becomes VISIBLE here, and the submit strip rides
// the same wire); a host that is down or does not serve that path keeps the
// embedded state and NAMES the outage (`live_status` + `funnel_source` — the
// warehouse-nav "live:"/"fixture" provenance vocabulary). Never fabricated.
// Bound to the SELECTED warehouse; the selector re-binds it on change.
let funnel: Box<dyn Panel> =
nornir_funnel_embed::panel_embedded_live(&endpoint, &token, &initial);
// The Claude pane: the shared facett-claude conversation pane, hosted over the shared
// `nornir-claude` edda leaf's transport (L5). Default = the leaf's OFFLINE stub (no
// key/no network); `--features claude-http` swaps in the leaf's live HTTP transport
// (credential resolved at send time — see `.nornir/claude-token-path.md`).
#[cfg(feature = "claude-http")]
let claude: Box<dyn Panel> = Box::new(tabbed::ClaudeTab::live());
#[cfg(not(feature = "claude-http"))]
let claude: Box<dyn Panel> = Box::new(tabbed::ClaudeTab::offline());
// The **Jera** central panel — dwarves' ONE jobs/zones surface (shared
// `facett_zoneui::JeraCentral`). This UNIFIES the old Jobs / Jobs (live) duality:
// the SAME jera ledger, now with the framework's per-job ZONE COLUMN.
// Mounted as a `SplitPanel`: its body DECOMPOSES into `jera-main` (the jobs/zones
// tree) │ `jera-inspector` (the live-feed control strip) — two scene panes with a
// draggable divider, so the strip can no longer eat the tree's height.
//
// **IT READS OVER THE WIRE, NOT OFF A PATH.** It used to open `jobs.redb` at
// `dwarves_embed::default_ledger_root()` — a path the UI guessed out of its OWN
// `$HOME`. That is the wrong file on any real deployment (the served workspace's
// ledger lives under the SERVER's nornir warehouse, opened for the process lifetime),
// and it is a file the UI's user may not be able to open at all (the installed forge
// runs as `dwarves` and writes `-rw-r--r--`; `JobStore::open` needs write access even
// to read). The rule is stated one line above the answer in
// `nornir-wire/proto/nornir.proto:981-983` — *the server holds the redb lock, SO
// CLIENTS GO THROUGH HERE* — and the answer is `Viz.Jobs`, reached the way every
// other warehouse-facing pane in this shell reaches nornir: through the FORGE
// (`:7900`), which proxies to its embedded nornir.
//
// `jera_ledger` survives as the EXPLICIT file-road escape hatch, and only that: it is
// taken when `$DWARVES_JOBS_DIR` names a ledger, because an operator naming a path is
// a statement about a ledger they own — a different thing from the UI deriving one
// from its own `$HOME` and hoping. Unset ⇒ the wire, always.
// The chooser moved into `jera::job_source` (built above) so the Pipeline tab reads
// the SAME ledger by the SAME road. `with_source_lazy` keeps the wire read off the
// construction path, exactly as `JeraTab::wire` did.
let jera: Box<dyn tabbed::SplitPanel> = Box::new(jera::JeraTab::with_source_lazy(jobs_source));
// The 🧬 **Dupes** tab — a `Mimir.SimilarCode` sub-view. It READS the SELECTED
// workspace's `lens_similar_code` (the jera-computed answer) over the thin
// `Mimir.SimilarCode` RPC and renders it — it does NOT fold. It reads itself on open
// (off the egui thread) and shows a lens_meta-derived provenance header; a failed read
// is an honest UNAVAILABLE. It keeps an APPLY escape hatch that folds LOCALLY on demand
// (the lens carries no executable RefactorPlan); `$DWARVES_CONSTELLATION` hydrates that
// local fold's spans / dep-edge fallback.
// The 📐 **Coverage** tab — the COVERAGE INDEX over the SELECTED workspace, read
// over the SAME thin gRPC seam. It opens UN-scanned (never a blocking warehouse
// read at shell construction) and, when the server is down, says UNAVAILABLE with
// the reason instead of rendering an empty index as a clean bill of health.
// `$DWARVES_CONSTELLATION` fixes the repo ROSTER, so a repo that exists on disk but
// has no warehouse rows shows as NOT SCANNED rather than vanishing from the
// denominator.
let coverage: Box<dyn Panel> = Box::new(coverage::coverage_pane(&endpoint, &token, &initial));
// The 🔍 **Lenses** tab — the three MIMIR LENSES over the SELECTED workspace, read
// over the SAME thin gRPC seam. Opens UN-scanned; a dead server says UNAVAILABLE
// with the reason rather than rendering three empty lists as "no findings".
let lenses: Box<dyn Panel> = Box::new(mimir_lens::lens_pane(&endpoint, &token, &initial));
// The 🏢 **Data view** tab — the warehouse DASHBOARD, back under Mímir. Mounts the
// SHARED `facett_warehousedeck::WarehouseDeck` (a wall of panes, each binding one
// Mímir/warehouse table to a view-kind: 3D graph / grid / chart) over the SELECTED
// workspace, fed Arrow frames from the SAME thin `Warehouse.Scan` gRPC read seam the
// Coverage / Lenses / Binary tabs use — plus a DataFusion SQL prompt (feature
// `dataview-sql`; the engine skade re-exports) that runs over the fetched frames.
// Opens UN-scanned (no blocking read at shell construction); a dead server says
// UNAVAILABLE with the reason rather than drawing an empty wall as a clean warehouse.
let dataview: Box<dyn Panel> = Box::new(dataview::dataview_pane(&endpoint, &token, &initial));
// The 🌊 **Codeflow** tab — the SELECTED workspace's call graph as living flow
// (`facett_flow::FlowGraph`), lines rated by the latest traced run's measured
// call counts over the SAME forge room seam; static call sites stand in (and
// are named) when the warehouse holds no run.
let codeflow: Box<dyn Panel> = Box::new(codeflow::codeflow_pane(&endpoint, &token, &initial));
// The 🌐 **Flow3D** tab — the SAME call graph + traced rates as a living 3D
// cloud (`facett_graph3d::Graph3D` + its edge-rate pulse overlay), over the
// same forge room seam.
let flow3d: Box<dyn Panel> = Box::new(flow3d::flow3d_pane(&endpoint, &token, &initial));
// The Status board: a wall of pipeline-health badges — the UI surface of the
// `dwarves badge` verb, reusing `dwarves::brokkr::badge` (one badge source).
// Mounted as a `SplitPanel`: `status-main` (the health board) │ `status-inspector`
// (the expanded badge SVG). The SVG used to be appended under the board in the same
// flow, so opening one badge pushed the board off the top of the pane.
//
// FED (2026-09-10) by the forge's own `dwarf.v1/Dwarf.Show` — the registered
// pipeline roster + recent runs in one round trip, read off-thread. It used to be
// `StatusPane::from_fixture()`: seven hand-written chips, three green. An
// unreachable or empty forge now draws the NAMED empty state, never a chip.
let status: Box<dyn tabbed::SplitPanel> =
Box::new(status::StatusPane::from_forge(&endpoint, &token));
// 🌳 Tree: asks the FORGE `Dwarves.Tree` for the selected workspace (the forge folds
// nornir's `Coderoom.Units` board on its jera lane); re-binds on the selector.
let tree: Box<dyn Panel> = Box::new(front_tree::FrontTree::new(
endpoint.clone(),
token.clone(),
initial.clone(),
));
// 🧭 Explorer: the forge's project tree and each node's detail (`dwarf.v1.Explorer`);
// ▶ Build workspace / ▶ Build / ▶ Run queue jera jobs on the forge and are followed.
let explorer: Box<dyn Panel> =
Box::new(explorer::ExplorerTab::new(endpoint.clone(), token.clone()));
// The **Release** graph pane (task #6 + #28): the release convergence — the SAME
// constellation DAG in THREE states (LOCAL manifest · SPARRING Gate-B rehearsal ·
// CRATES_IO published) that a release drives to CONVERGE — rendered on the REUSED
// `facett-releaseview` 3-plane viewer. It loads the convergence output a
// `workspace-release --features facett-adapter` run wrote (dwarves' `convergence_adapter`
// lowering the graph_diff before/after into the model), falling back to a nordisk
// fixture so the tab shows a live, converging graph even with no release run.
// Mounted as a `SplitPanel`: `release-main` (the three-graph convergence viewer) │
// `release-inspector` (the shooting-ramp band — Gates · Ramp · Bump plan, whose
// detail grows a line per member repo and used to eat the graph's height).
let release: Box<dyn tabbed::SplitPanel> = Box::new(release::ReleaseTab::new(initial.clone()));
// The 🚀 **Mímir release** tab: the plan the FORGE reads from Mímir (BuildOrder ·
// DepsOf · ChangedSinceLastRelease · AffectedByChange) on its jera queue, asked over
// `dwarf.v1.MimirRelease.Plan` — answered at once (the plan, or the job), asked again
// while queued. Nothing is read from a checkout on this machine.
let mimir_release: Box<dyn Panel> = Box::new(mimir_release::MimirReleaseTab::new(
endpoint.clone(),
token.clone(),
initial.clone(),
));
// The **Testmatrix + Benches** view (task #66) — the SELECTED workspace's testmatrix
// (reused `facett_matrix::MatrixView` grid, surface×coverage cells) + its
// `nornir.mimir.<populate|search>` bench trend (populate + MCP-search timings,
// newest-vs-first), consumed over the file seam via the `nornir_bench` read API. This
// is where a human watches the 100-bench mimir epic (#62) grind green; re-binds to the
// selected warehouse like Release.
let benches: Box<dyn Panel> = Box::new(benches::BenchesTab::new(initial.clone()));
// The **Fleet** coverage board (autotest North Star): MOUNTS the shared
// `facett_fleet_view::FleetView` — the full-system-autotest run as a member×surface
// grid under a `Gap == ∅` verdict banner (the fleet analogue of the Benches tab's
// reused `facett_matrix` grid; dwarves owns no second coverage model). Fed a
// `FleetRunView` (nornir-testmatrix `GateReport` + `TestResultRow`s + members) offline
// from `$DWARVES_FLEET_RUN` (the SAME file-seam gesture Release/Benches/Saga use). With
// NO run it draws the NAMED empty state over that env var — it used to draw
// `FleetView::local()`, the shared component's demo grid plus a red `Gap == ∅` banner
// naming a missing node no run had looked for (deleted 2026-09-10). Constellation-wide,
// so — unlike Release/Benches — it does NOT re-bind to the selected workspace. The
// fat/thin LIVE feeds land in `FleetTab::with_run` (TODO'd in `fleet.rs`).
let fleet: Box<dyn Panel> = Box::new(fleet::FleetTab::new());
// The **Binary** view — the binary half of the SELECTED workspace's code room
// (`Viz.Binary` through the forge's `CodeRoom.Answer`) on the shared
// `facett_binview::BinaryPane`. A room with no linked artifact, or an unreachable
// forge, draws its state — never a sample. Re-binds to the selection like Release.
let binary: Box<dyn Panel> = Box::new(binview::BinaryTab::new(
endpoint.clone(),
token.clone(),
initial.clone(),
));
// The **Coderoom** view — the code room of the SELECTED workspace on the shared
// `facett_coderoom_view::CoderoomPane`: the room's crate graph (`Viz.CrateGraph` —
// one node per crate@version, a join per pair with its dependency / call / type-use
// counts) and what changed between its last two consumed commits
// (`Coderoom.SnapshotDiff`), asked through the forge's `CodeRoom.Answer`. Compat
// verdicts and what-breaks wait for the world ring: the pane shows nornir's
// `not_wired` for both and draws no verdict. A room that does not answer draws its
// state, never a graph. Re-binds to the selection like Release.
let coderoom: Box<dyn Panel> = Box::new(coderoom::CoderoomTab::new(
endpoint.clone(),
token.clone(),
initial.clone(),
));
// The **Saga** tape-recorder view (TASK #42) — the SELECTED workspace's recorded
// user journeys: a painful-first sessions rail (`summarize_sessions`) + a transport
// bar bound to the reused `nornir_saga::ui::Player` (play/step/seek/speed) + a stage
// re-rendering each frame's captured `state_json`, with a RED marker on the step the
// user got stuck on. Fed the `.jsonl` tape `dwarves saga import` lands for the
// workspace (the offline file seam); re-binds to the selection like Release/Benches.
let saga: Box<dyn Panel> = Box::new(saga::SagaTab::new(initial.clone()));
// The common **Settings** pane: workspace (mirrors the selector) · fat/thin backend ·
// theme (reused `facett_core::look` presets) · vault-unlock. Seeded with the registered
// warehouses + the initial selection.
let settings: Box<dyn Panel> = Box::new(
settings::SettingsPane::new(warehouses.clone(), initial.clone())
.with_server_endpoint(endpoint.clone()),
);
// The **Profile** view: the SAME shared Identity About uses + the vault's credential
// NAMES (via the vault seam; the `SampleVault` until `feat-vault` lands — never values).
let profile: Box<dyn Panel> = Box::new(profile::ProfilePane::new(about::identity()));
// The 📖 **Manual** tab — dwarves' `.nornir/*.md` doc set (idiot guides, guides,
// designs) embedded in the binary (behind `manual-embed`, default ON) and rendered in
// the SHARED `facett_manual::ManualPane` — the SAME manual-pane core korp mounts (L5,
// one component not two). With `--no-default-features` the pane honestly says the
// manual is not embedded rather than drawing a blank pane.
let manual: Box<dyn Panel> = Box::new(manual::manual_pane());
// The panels, in the SHIPPED order — zipped against the `DWARVES_TABS` roster, which
// is the ONE place a tab label lives. No second hardcoded label list: a panel added
// without a roster entry (or a roster entry with no panel) trips the length oracle
// below, so "no tab dropped" is enforced, not merely documented.
// `TabPanel` per tab: `Whole` for a body that is honestly one pane (and for every tab
// that is a thin wrapper around a SHARED facett widget, whose master/detail is not
// dwarves' to split), `Split` for a body that DECOMPOSES into `<slug>-main` |
// `<slug>-inspector`, `Leaves` for a body that decomposes into the N named leaves its
// own module declares. `.into()` picks the variant from the declared box type, so this
// stays a plain roster and the "no tab dropped" length oracle below is unchanged.
let panels: Vec<tabbed::TabPanel> = vec![
// Index-aligned with DWARVES_TABS, area by area (DWARVES_AREAS).
// 🏠 Workspace
tree.into(),
status.into(),
// 🧭 Code
coderoom.into(),
lenses.into(),
dataview.into(),
warehouse.into(),
codeflow.into(),
flow3d.into(),
// ⚒ Build
explorer.into(),
pipeline.into(),
cicd.into(),
builds.into(),
build_thing.into(),
syscall_profiles.into(),
forge.into(),
jera.into(),
// 🧪 Test & quality
benches.into(),
coverage.into(),
binary.into(),
fleet.into(),
saga.into(),
// 🚀 Release
release.into(),
mimir_release.into(),
// 🛠 Work & settings
funnel.into(),
claude.into(),
settings.into(),
profile.into(),
deploy_key.into(),
manual.into(),
];
assert_eq!(
panels.len(),
DWARVES_TABS.len(),
"every shipped panel needs a DWARVES_TABS entry (and vice versa) — no tab dropped"
);
let tabs: Vec<(String, tabbed::TabPanel)> = DWARVES_TABS
.iter()
.map(|(_, label)| (*label).to_string())
.zip(panels)
.collect();
tabbed::TabbedView::new(NATIVE_TITLE, tabs)
// The six areas on the rail; a tab strip shows one area's tabs.
.with_areas(DWARVES_AREAS.iter().map(|(label, keys)| {
(
(*label).to_string(),
keys.iter().map(|k| (*k).to_string()).collect(),
)
}))
// The warehouse selector header (re-binds every tab on change) + the About popup,
// shown at startup (task #31 — the shared facett-about card w/ the forge logo).
.with_warehouse_selector(selector)
.with_about_at_startup(about::identity())
}
// ── the roster ORACLE ─────────────────────────────────────────────────────────
/// The "no tab dropped" proofs for [`DWARVES_TABS`] — the analogue of korp's
/// `scene_slots_mirror_the_tab_enum`. The roster is the ONE tab list; these assert the
/// shipped shell, the scene and the title all agree with it, so dropping a tab (or
/// adding one to only half the surfaces) fails RED here instead of silently shipping a
/// shorter shell — the exact failure the old lib/bin twin hid.
#[cfg(all(test, not(target_arch = "wasm32")))]
mod roster_tests {
use super::*;
use facett_core::Facet;
fn cell(name: &str, ok: bool, detail: &str) {
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status("dwarves-gui", name, ok, detail);
let _ = (name, ok, detail);
}
/// The roster is well-formed: every key IS its label lowercased, and both keys and
/// labels are unique (unique keys are what makes every pane deep-link addressable).
#[test]
fn roster_keys_are_lowercased_labels_and_unique() {
let mut keys: Vec<String> = tab_keys();
let mut labels: Vec<String> = tab_labels();
// Each key IS the slug of its label — the same function `dwarves_scene` and
// `state_json` derive ids with, so the roster can never disagree with the shell.
let derived_ok = DWARVES_TABS.iter().all(|(k, l)| *k == tabbed::slug(l));
assert!(
derived_ok,
"each DWARVES_TABS key must equal tabbed::slug(label)"
);
// And no key carries a character that is unsafe in a deep-link path segment —
// notably `/` (which `"CI/CD"` would otherwise leak). RED for any future label.
let addressable_ok = DWARVES_TABS.iter().all(|(k, _)| {
!k.is_empty()
&& k.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
});
assert!(
addressable_ok,
"every slot id stays deep-link safe (no `/`, no spaces): {DWARVES_TABS:?}"
);
let n = DWARVES_TABS.len();
keys.sort();
keys.dedup();
labels.sort();
labels.dedup();
let unique_ok = keys.len() == n && labels.len() == n;
assert!(
unique_ok,
"DWARVES_TABS keys + labels are unique ({n} tabs)"
);
cell(
"roster_keys_are_lowercased_labels_and_unique",
derived_ok && addressable_ok && unique_ok,
"the DWARVES_TABS roster is well-formed: every slot key is its label lowercased and both keys and labels are unique across all 18 shipped tabs",
);
}
/// The SHIPPED shell mounts exactly the roster — same labels, same order, same count
/// — and its `state_json` folds each pane under the roster key. RED if a tab is
/// dropped from either side.
#[test]
fn shipped_shell_mounts_exactly_the_roster() {
let app = build_native_app(
"http://127.0.0.1:0".into(),
std::path::PathBuf::from("/nonexistent/dwarves-gui/roster-test"),
);
let s = Facet::state_json(&app);
let tabs: Vec<String> = s["tabs"]
.as_array()
.unwrap()
.iter()
.map(|t| t.as_str().unwrap().to_string())
.collect();
let labels_ok = tabs == tab_labels();
assert!(
labels_ok,
"the shell mounts exactly DWARVES_TABS in order: {tabs:?}"
);
// Every roster key carries that pane's own state (the fold key a robot addresses).
let keys_ok = tab_keys()
.iter()
.all(|k| s.get(k.as_str()).map(|v| v.is_object()).unwrap_or(false));
assert!(keys_ok, "every roster key folds its pane's state: {s}");
cell(
"shipped_shell_mounts_exactly_the_roster",
labels_ok && keys_ok,
"the shipped build_native_app shell mounts exactly the DWARVES_TABS roster (same 18 labels, same order) and folds every pane's own state under its roster key — the no-tab-dropped oracle",
);
}
/// The declared SCENE hosts one addressable slot per roster tab, and the shipped
/// title names every one of them.
#[test]
fn scene_and_title_cover_every_roster_tab() {
let scene = tabbed::dwarves_scene(&tab_labels());
scene.validate().expect("unique pane + menu names");
let panes = scene.pane_names();
let scene_ok = tab_keys().iter().all(|k| panes.contains(k));
assert!(
scene_ok,
"the scene declares a pane for every roster tab: {panes:?}"
);
// The menubar's View/Go menus are built from the same roster (one item per tab).
let view = scene.menu_item("view").expect("View menu");
let go = scene.menu_item("go").expect("Go menu");
let menu_ok =
view.items.len() == DWARVES_TABS.len() && go.items.len() == DWARVES_TABS.len();
assert!(menu_ok, "View + Go carry one item per roster tab");
let title_ok = DWARVES_TABS
.iter()
.all(|(_, label)| NATIVE_TITLE.contains(label));
assert!(
title_ok,
"NATIVE_TITLE names every roster tab: {NATIVE_TITLE}"
);
cell(
"scene_and_title_cover_every_roster_tab",
scene_ok && menu_ok && title_ok,
"the declarative scene declares an addressable slot pane for every DWARVES_TABS tab, the View/Go menubars carry one item per tab, and NATIVE_TITLE names all 18 — the roster is the single source for shell, scene, menubar and title",
);
}
/// **Six areas, no tab dropped, none twice.** Read in order, the areas' tab keys
/// ARE the roster — so every tab sits in exactly one area, and the roster stays in
/// area order (an area's strip is a contiguous run of the shell's tabs).
#[test]
fn the_six_areas_partition_the_roster_in_order() {
let labels: Vec<&str> = DWARVES_AREAS.iter().map(|(l, _)| *l).collect();
assert_eq!(
labels,
[
"🏠 Workspace",
"🧭 Code",
"⚒ Build",
"🧪 Test & quality",
"🚀 Release",
"🛠 Work & settings"
],
"the owner's six areas, in order"
);
let flat: Vec<&str> = DWARVES_AREAS
.iter()
.flat_map(|(_, keys)| keys.iter().copied())
.collect();
let roster: Vec<&str> = DWARVES_TABS.iter().map(|(k, _)| *k).collect();
assert_eq!(
flat, roster,
"the areas' tabs, read in order, are exactly the roster"
);
assert!(
DWARVES_AREAS.iter().all(|(_, keys)| !keys.is_empty()),
"no empty area"
);
}
}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod app_tests {
use super::*;
use facett_core::Facet;
/// **The menu in six areas, rendered.** The shipped shell opens on 🏠 Workspace;
/// the strip shows ONLY the active area's tabs; reaching a tab by name brings its
/// area along; an area returns to the tab it was last on; and a real click on an
/// area on the rail switches to it.
#[test]
fn the_rail_shows_six_areas_and_the_strip_only_the_active_areas_tabs() {
use egui_kittest::kittest::Queryable;
use egui_kittest::Harness;
use std::{cell::RefCell, rc::Rc};
let app = Rc::new(RefCell::new(build_native_app(
"http://127.0.0.1:0".into(),
std::path::PathBuf::from("/nonexistent/dwarves-gui/areas-test"),
)));
Facet::update_json(&mut *app.borrow_mut(), r#"{"about":false}"#);
let drawn = Rc::clone(&app);
let mut h = Harness::builder()
.with_size(egui::vec2(1600.0, 1000.0))
.build_ui(move |ui| Facet::ui(&mut *drawn.borrow_mut(), ui));
let strip = |app: &Rc<RefCell<tabbed::TabbedView>>| -> Vec<String> {
app.borrow().tab_label_rects().into_keys().collect()
};
let area_tabs = |area: &str| -> Vec<String> {
let (_, keys) = DWARVES_AREAS.iter().find(|(l, _)| *l == area).unwrap();
let mut v: Vec<String> = keys
.iter()
.map(|k| {
DWARVES_TABS
.iter()
.find(|(kk, _)| kk == k)
.unwrap()
.1
.to_string()
})
.collect();
v.sort();
v
};
h.step();
h.step();
assert_eq!(
app.borrow().active_area(),
"🏠 Workspace",
"opens on the front page"
);
assert_eq!(
strip(&app),
area_tabs("🏠 Workspace"),
"the strip holds the Workspace area only"
);
for (area, _) in DWARVES_AREAS {
assert!(h.query_by_label(area).is_some(), "the rail lists `{area}`");
}
Facet::update_json(&mut *app.borrow_mut(), r#"{"set_tab":"Builds"}"#);
h.step();
h.step();
assert_eq!(
app.borrow().active_area(),
"⚒ Build",
"a tab brings its area along"
);
assert_eq!(
strip(&app),
area_tabs("⚒ Build"),
"…and the strip is that area's tabs"
);
Facet::update_json(&mut *app.borrow_mut(), r#"{"set_tab":"Warehouse"}"#);
h.step();
Facet::update_json(&mut *app.borrow_mut(), r#"{"set_area":"build"}"#);
h.step();
assert_eq!(
app.borrow().active_tab(),
"Builds",
"an area returns to the tab it was last on"
);
h.get_by_label("🧭 Code").click();
h.step();
h.step();
assert_eq!(
app.borrow().active_area(),
"🧭 Code",
"a click on the rail switches the area"
);
assert_eq!(
app.borrow().active_tab(),
"Warehouse",
"…back on the tab it was last on"
);
assert_eq!(strip(&app), area_tabs("🧭 Code"));
let s = Facet::state_json(&*app.borrow());
assert_eq!(s["active_area"], "🧭 Code");
assert_eq!(s["areas"].as_array().map(|a| a.len()), Some(6));
}
/// ROBOT SCENARIO — the shipped native shell driven over the remote seam, asserting
/// the framework rollout landed and nothing regressed:
/// 1. the **Jobs / Jobs (live) duality is GONE** — there is exactly ONE jobs surface,
/// the unified **Jera** tab, and NO "Jobs"/"Jobs (live)" tab;
/// 2. there is **NO bare demo tab/button** anywhere (RED if one reappears);
/// 3. the **Jera** central panel carries the jera ledger AND the zone column;
/// 4. there is **NO 🧰 Tools tab** (the korp-style Zone affordance was removed — it
/// does not belong in dwarves; RED if it creeps back);
/// 5. the **Warehouse landing + Forge** are NOT regressed (still the first tab, still
/// the constellation). Emits a `nornir-testmatrix` cell (reuse edda).
#[test]
fn robot_zone_framework_rollout_no_duality_no_demo_button() {
// Dead ports (offline-safe) + an empty ledger root ⇒ the Jera zoned fixture.
let mut app = build_native_app(
"http://127.0.0.1:0".into(),
// The funnel opens embedded (its live leg is lazy — a dead endpoint is
// never dialed at construction; this test asserts the
// Jera/Tools/Warehouse/Builds/Forge spine, not the funnel).
std::path::PathBuf::from("/nonexistent/dwarves-gui/rollout-test"),
);
let s = Facet::state_json(&app);
let tabs: Vec<&str> = s["tabs"]
.as_array()
.unwrap()
.iter()
.map(|t| t.as_str().unwrap())
.collect();
// (1) the duality is gone — ONE unified jobs surface (Jera), no Jobs/Jobs (live).
let no_duality = tabs.contains(&"Jera")
&& !tabs.iter().any(|t| t.eq_ignore_ascii_case("Jobs"))
&& !tabs.iter().any(|t| t.eq_ignore_ascii_case("Jobs (live)"));
// (2) no bare demo tab/button.
let no_demo_tab = !tabs.iter().any(|t| t.to_ascii_lowercase().contains("demo"));
// (3) the Jera central panel = jera ledger + zone column.
//
// This shell is built over a DEAD forge, and that is now a STATED state rather
// than a silent one. Two regressions are pinned here, in the order they were
// found:
// · the pane used to fall through to `fixture_zoned_jobs()` on any empty read,
// so this assertion once read `count >= 3` and was in fact asserting the
// FABRICATION (owner 2026-08-25: nuke the workspace and the jobs are nuked
// with it);
// · it then read `source == "absent"` — the FILE road's answer, from when the
// shell handed the tab a `jobs.redb` path out of its own `$HOME`. The shell
// now reads `Viz.Jobs` through the forge, so an unreachable forge is
// `unreadable` with the outage NAMED in `error`. `absent` would be a claim
// about a workspace this client never heard from, which is the same
// over-claim in a new place.
// `{"refresh":true}` lands the in-flight cold read so the state is the answer,
// not the moment before it.
Facet::update_json(&mut app, r#"{"set_tab":"Jera"}"#);
Facet::update_json(&mut app, r#"{"refresh":true}"#);
let jera = &Facet::state_json(&app)["jera"];
let jera_ok = jera["kind"] == "jera"
&& jera["has_demo_button"] == false
&& jera["source"] == "unreadable"
&& jera["error"]
.as_str()
.is_some_and(|e| e.contains("unreachable"))
&& jera["ledger_absent"] == serde_json::json!(false)
&& jera["job_count"] == serde_json::json!(0)
&& jera["count"].as_u64().unwrap() == 0;
// …and the ZONE COLUMN is still real — proved on the EXPLICIT demo constructor,
// which is the only thing allowed to render fixture jobs now.
let zone_column_ok = {
let demo = crate::jera::JeraTab::from_fixture();
let st = Facet::state_json(&demo);
st["zones_present"].as_array().unwrap().len() == 3 && st["count"].as_u64().unwrap() >= 3
};
// (4) the 🧰 Tools tab (korp-style Zone affordance) is GONE from dwarves.
let no_tools_tab = !tabs.iter().any(|t| *t == "Tools");
// (5) the Warehouse landing + Forge are not regressed.
Facet::update_json(&mut app, r#"{"set_tab":"Warehouse"}"#);
let warehouse = &Facet::state_json(&app)["warehouse"];
let landing_ok =
warehouse["kind"] == "warehouse-nav" && warehouse["level"] == "constellation";
let forge_present = tabs.contains(&"Forge");
// (6) the **Builds** (holger build-MATRIX, B2) tab is HOSTED and drives to an
// HONEST state — offline (dead port) ⇒ `unreachable` with zero tiers, never a blank.
Facet::update_json(&mut app, r#"{"set_tab":"Builds"}"#);
let builds = &Facet::state_json(&app)["builds"];
let builds_ok = tabs.contains(&"Builds")
&& builds["kind"] == "holger_build_matrix"
&& builds["status"] == "unreachable"
&& builds["tier_count"] == 0;
let drivable = no_duality
&& no_demo_tab
&& jera_ok
&& zone_column_ok
&& no_tools_tab
&& landing_ok
&& forge_present
&& builds_ok;
assert!(drivable, "the zone framework rolled out (no duality, no demo button, unified Jera over an ABSENT ledger that says so rather than inventing jobs, zone column real on the explicit demo, NO korp-style Tools tab) with no landing regression: tabs={tabs:?} jera={jera}");
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(
"dwarves-gui",
"zone_framework_rollout_no_duality_no_demo_button",
drivable,
"the shared zone framework rolled into dwarves' shell: Jobs/Jobs(live) duality unified into ONE facett_zoneui::JeraCentral (ledger + zone column); the korp-style 🧰 Tools/ZonePane affordance REMOVED (does not belong in dwarves); NO bare demo button; Warehouse/Forge landing not regressed — driven headless over state_json/update_json",
);
}
}
// ── the FULL USER JOURNEY (task #56) ─────────────────────────────────────────
// The North Star applied to the WHOLE shell: one headless robot drives the shipped
// `build_native_app` through a complete operator journey over the remote seam
// (`state_json`/`update_json`) — pick a workspace, unlock the REAL vault + add a
// credential, sign in over the stub IdP, build+point+save+RUN a CI/CD pipeline that
// pulls the vault credential into the runner env, and see Release + Benches render.
// No human, no AI, no network. Each leg emits a `nornir-testmatrix` cell.
//
// Gated on `--features vault`: the journey drives the REAL `dwarves::vault` (Settings
// unlock + credential add + the wizard's cred→runner-env hop). Sign-in rides the
// network-free `StubLogin` (the demo's default when `login` is off — the localhost
// stub device-code IdP), whose unlock gate now tracks the live vault session.
#[cfg(all(test, not(target_arch = "wasm32"), feature = "vault"))]
mod journey_tests {
use super::*;
use dwarves::vault::{Session, ZnippyVault};
use facett_core::Facet;
/// Emit a per-leg testmatrix cell (a no-op unless `--features testmatrix`).
#[allow(unused_variables)]
fn cell(name: &str, ok: bool, detail: &str) {
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status("dwarves-gui", name, ok, detail);
}
/// A LOCAL stub of the RFC 8628 IdP endpoints for the journey's PROFILE sign-in leg
/// under the `login` feature (the REAL `DwarvesLoginSeam` mounted in Profile drives a
/// device-code grant — this keeps it network-free). A raw `tokio` TCP HTTP/1.1 server
/// answering `/device` + `/token`, plus a `jwt` minter for the display-only access
/// token. Bound to `127.0.0.1:0`. Mirrors korp/holger's stub. The `tokio::spawn`s run
/// IO (a mock HTTP server), NOT CPU work — ROOT LAW #0 targets compute parallelism.
#[cfg(feature = "login")]
mod login_stub_idp {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
/// An unsigned display-only JWT `header.payload.sig` (the shape the IdP mints).
pub fn jwt(payload: serde_json::Value) -> String {
use base64::Engine;
let b64 = |b: &[u8]| base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(b);
format!(
"{}.{}.sig",
b64(br#"{"alg":"none"}"#),
b64(payload.to_string().as_bytes())
)
}
/// Bind on an ephemeral loopback port, serve the two endpoints on a background
/// task, and return the bound `addr`.
pub async fn spawn(access_token: String) -> std::net::SocketAddr {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let device_body = format!(
r#"{{"device_code":"dc-123","user_code":"WDJB-MJHT","verification_uri":"http://{addr}/device","interval":1,"expires_in":600}}"#
);
let token_body = format!(
r#"{{"access_token":"{access_token}","refresh_token":"rt-456","expires_in":3600,"token_type":"Bearer"}}"#
);
tokio::spawn(async move {
loop {
let Ok((mut sock, _)) = listener.accept().await else {
break;
};
let device_body = device_body.clone();
let token_body = token_body.clone();
tokio::spawn(async move {
let mut buf = vec![0u8; 4096];
let n = sock.read(&mut buf).await.unwrap_or(0);
let req = String::from_utf8_lossy(&buf[..n]);
let path = req.split_whitespace().nth(1).unwrap_or("/");
let body = if path.starts_with("/token") {
&token_body
} else {
&device_body
};
let resp = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
body.len(),
body
);
let _ = sock.write_all(resp.as_bytes()).await;
let _ = sock.shutdown().await;
});
}
});
addr
}
}
/// ROBOT SCENARIO (task #56 — the whole-shell user journey): a headless robot drives
/// the SHIPPED native shell through the complete operator flow over `state_json`/
/// `update_json`, proving a human can drive the entire app with no AI:
/// 1. PICK a workspace from the registry (re-binds every tab);
/// 2. SETTINGS → unlock the REAL vault + ADD a credential (via the real Vault);
/// 3. PROFILE → sign in over the stub device-code IdP (gated on the unlock);
/// 4. CI/CD → build a build→test→deploy pipeline pointed at a git SSH source, SAVE
/// the *.runa.ron, and RUN it (dry) — the run USES the git source and PULLS the
/// vault credential into the runner env (fail-closed when locked);
/// 5. RELEASE + BENCHES render for the selected workspace.
/// Each leg asserted via `state_json`; each emits a `nornir-testmatrix` cell. No
/// secret value ever appears in any pane's state.
#[test]
fn robot_full_user_journey_drives_the_whole_shell() {
let _g = crate::ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let dir = tempfile::tempdir().unwrap();
let sess = dir.path().join("vault.session");
let pipelines = dir.path().join("pipelines");
std::fs::create_dir_all(&pipelines).unwrap();
let prev_home = std::env::var("HOME").ok();
// SAFETY: single-threaded under ENV; isolate HOME (the default vault path) + the
// session file so the operator's real `~/.dwarves` is never touched.
unsafe {
std::env::set_var("HOME", dir.path());
std::env::set_var("DWARVES_VAULT_SESSION", &sess);
std::env::set_var("DWARVES_WAREHOUSES", "nordisk,skade");
std::env::remove_var("NORNIR_WORKSPACE");
std::env::remove_var("NORNIR_SERVER");
}
std::fs::create_dir_all(dir.path().join(".dwarves")).unwrap();
// Seed the default vault (creates vault.bin) with the two credentials the journey
// uses: a crates.io token (pulled into the runner env) + an SSH deploy key.
let dv = ZnippyVault::default_vault();
dv.add(
b"correct-pw",
"CARGO_REGISTRY_TOKEN",
"crates-io-token",
b"cio_TOPSECRET",
)
.unwrap();
dv.add(
b"correct-pw",
"deploy-key",
"ssh-key",
b"KEYBYTES-never-shown",
)
.unwrap();
// Under the `login` feature the Profile mounts the REAL `DwarvesLoginSeam` (which
// reads `DWARVES_OIDC_*` at construction). Point it at a LOCAL stub IdP so the
// journey's device-code sign-in runs end to end WITHOUT a real network — the seam
// seals the acquired token into the (soon-to-be-unlocked) vault. Under `vault`-only
// the Profile mounts the honest StubLogin, which ignores these and approves
// synthetically. The runtime is kept alive for the whole journey.
#[cfg(feature = "login")]
let stub_access_token =
login_stub_idp::jwt(serde_json::json!({ "preferred_username": "robot@vetra.se" }));
#[cfg(feature = "login")]
let _login_server_rt = {
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.enable_all()
.build()
.unwrap();
let addr = rt.block_on(login_stub_idp::spawn(stub_access_token.clone()));
// SAFETY: single-threaded under ENV; the OIDC endpoints are process-global.
unsafe {
std::env::set_var(
"DWARVES_OIDC_DEVICE_AUTH_URL",
format!("http://{addr}/device"),
);
std::env::set_var("DWARVES_OIDC_TOKEN_URL", format!("http://{addr}/token"));
std::env::set_var("DWARVES_OIDC_CLIENT_ID", "dwarves-cli");
std::env::set_var("DWARVES_OIDC_SCOPE", "openid offline_access");
}
rt
};
// The shipped shell (a dead forge endpoint + a nonexistent ledger ⇒ offline-safe
// fixtures; the funnel opens embedded — its host-injected live leg is lazy and
// degrades to a NAMED outage against the dead endpoint — host-only, no nornir dial).
let mut app = build_native_app(
"http://127.0.0.1:0".into(),
std::path::PathBuf::from("/nonexistent/dwarves-gui/journey"),
);
// A running assertion accumulator; the whole journey is green iff every leg is.
let mut journey_ok = true;
let mut check = |name: &str, ok: bool, detail: &str| {
journey_ok &= ok;
cell(name, ok, detail);
ok
};
// ── 1. PICK a workspace from the registry (re-binds every tab) ───────────────
let st = Facet::state_json(&app);
let opens_on = st["selected_warehouse"] == "nordisk"
&& st["warehouses"] == serde_json::json!(["nordisk", "skade"]);
Facet::update_json(&mut app, r#"{"select_warehouse":"skade"}"#);
let st = Facet::state_json(&app);
let picked = opens_on
&& st["selected_warehouse"] == "skade"
// every warehouse-bound tab re-bound to the pick (Settings/Release/Benches).
&& st["settings"]["active_workspace"] == "skade"
&& st["release"]["release_target"] == "skade"
&& st["benches"]["selected_workspace"] == "skade";
check(
"journey_1_pick_workspace",
picked,
"PICK a workspace from the registry (nordisk→skade) re-binds every tab (Settings/Release/Benches) — driven headless over select_warehouse",
);
// ── 2. SETTINGS → unlock the REAL vault + ADD a credential ───────────────────
Facet::update_json(&mut app, r#"{"set_tab":"Settings"}"#);
// A wrong passphrase is rejected (stays locked, surfaces the error).
Facet::update_json(&mut app, r#"{"unlock":"WRONG"}"#);
let locked_still = Facet::state_json(&app)["settings"]["vault_unlocked"] == false;
// The right passphrase unlocks the real Session (the key the wizard + Profile read).
Facet::update_json(&mut app, r#"{"unlock":"correct-pw"}"#);
let unlocked = Facet::state_json(&app)["settings"]["vault_unlocked"] == true
&& Session::key_for_path(dv.path()).is_some();
// ADD a THIRD credential through the REAL vault while unlocked.
dv.add(b"correct-pw", "codeberg-token", "opaque", b"cb_pat_secret")
.unwrap();
check(
"journey_2_settings_unlock_and_add_credential",
locked_still && unlocked,
"SETTINGS unlocks the REAL dwarves::vault::Session (wrong passphrase rejected, correct one caches the key) and a credential is ADDED via the real Vault — driven headless over update_json",
);
// ── 3. PROFILE → sign in over the MOUNTED shared facett-auth-ui LoginPane ────
Facet::update_json(&mut app, r#"{"set_tab":"Profile"}"#);
let st = Facet::state_json(&app);
let profile = &st["profile"];
// The unlocked real vault is now readable: the THREE credential NAMES show, no value.
let names_ok = profile["vault_available"] == true
&& profile["secret_count"] == 3
&& profile["secret_names"]
.as_array()
.unwrap()
.iter()
.any(|n| n["name"] == "CARGO_REGISTRY_TOKEN")
&& profile["secret_names"]
.as_array()
.unwrap()
.iter()
.any(|n| n["name"] == "codeberg-token");
// Drive the ONE shared `facett_auth_ui::LoginPane` the Profile MOUNTS (korp/holger
// mount the same): `begin` → the RFC 8628 device prompt, `poll` → signed in. Under
// `login` this is the REAL `DwarvesLoginSeam` over the local stub IdP (token sealed
// in the unlocked vault); under `vault`-only it is the honest StubLogin. Both
// surface the awaiting-user prompt then a subject — nested under Profile's `login`.
Facet::update_json(&mut app, r#"{"begin":true}"#);
let awaiting = Facet::state_json(&app)["profile"]["login"].clone();
let prompt_ok = awaiting["state"] == "awaiting_user"
&& awaiting["user_code"]
.as_str()
.map_or(false, |c| !c.is_empty())
&& awaiting["verification_uri"]
.as_str()
.map_or(false, |u| !u.is_empty());
Facet::update_json(&mut app, r#"{"poll":true}"#);
let st = Facet::state_json(&app);
let login = &st["profile"]["login"];
let signed_in = login["state"] == "signed_in"
&& login["subject"].as_str().map_or(false, |s| !s.is_empty());
// No secret/token VALUE ever leaks into the Profile state (the vault values, and —
// under `login` — the sealed JWT + refresh token). NOTE: `access_token` is now a
// credential NAME the pane surfaces, so it is not itself a leak sentinel.
let no_leak = {
let whole = serde_json::to_string(&st["profile"]).unwrap();
let base = !whole.contains("TOPSECRET")
&& !whole.contains("KEYBYTES")
&& !whole.contains("cb_pat_secret")
&& !whole.contains("rt-456");
#[cfg(feature = "login")]
let base = base && !whole.contains(stub_access_token.as_str());
base
};
check(
"journey_3_profile_device_code_signin",
names_ok && prompt_ok && signed_in && no_leak,
"PROFILE lists the unlocked vault's credential NAMES (never values) and drives the MOUNTED shared facett-auth-ui LoginPane (begin → RFC 8628 prompt, poll → signed-in subject); under the login feature the REAL DwarvesLoginSeam seals the token in the unlocked vault — no secret leaks",
);
// ── 4. CI/CD → build→test→deploy pipeline · git SSH source · save · RUN (dry) ─
Facet::update_json(&mut app, r#"{"set_tab":"CI/CD"}"#);
Facet::update_json(
&mut app,
&format!(r#"{{"set_save_dir":"{}"}}"#, pipelines.display()),
);
Facet::update_json(
&mut app,
r#"{"set_git":"git@forge.example:nordisk/korp.git"}"#,
);
Facet::update_json(&mut app, r#"{"set_build":"oci"}"#);
Facet::update_json(&mut app, r#"{"set_deploy":"crates-io"}"#);
Facet::update_json(&mut app, r#"{"set_trigger":"push:main"}"#);
// Declare which vault credential the pipeline pulls into the runner env.
Facet::update_json(
&mut app,
r#"{"set_cred":{"name":"CARGO_REGISTRY_TOKEN","env":"CARGO_REGISTRY_TOKEN"}}"#,
);
// SAVE the *.runa.ron (into the temp dir) then RUN it (dry).
Facet::update_json(&mut app, r#"{"save":true}"#);
Facet::update_json(&mut app, r#"{"run":true}"#);
let st = Facet::state_json(&app);
let cicd = &st["ci-cd"];
let saved_file = pipelines.join("my-ci.runa.ron");
let injected = cicd["injected_env_keys"].as_array().unwrap();
let cicd_ok = cicd["kind"] == "cicd_wizard"
// build → test → deploy (3 stages, 2 needs edges).
&& cicd["dag_nodes"] == 3
&& cicd["pipeline_valid"] == true
// the run USES the git SSH source…
&& cicd["source"] == "git@forge.example:nordisk/korp.git"
// …and PULLED the vault credential into the runner env (keys only)…
&& injected.iter().any(|k| k == "CARGO_REGISTRY_TOKEN")
&& cicd["run_refused"] == false
// …and the dry run greened the DAG.
&& cicd["last_run"]["green"] == true
// the *.runa.ron artifact landed on disk.
&& saved_file.is_file();
// No secret value leaks through the wizard state.
let cicd_no_leak = !serde_json::to_string(cicd)
.unwrap()
.contains("cio_TOPSECRET");
check(
"journey_4_cicd_build_source_save_run_with_vault_cred",
cicd_ok && cicd_no_leak,
"CI/CD wizard builds a build→test→deploy pipeline pointed at a git SSH source, SAVES the *.runa.ron, and RUNs it (dry): the run USES the git source and PULLS the vault credential (CARGO_REGISTRY_TOKEN) into the runner env — driven headless over update_json, no secret leaks",
);
// Fail-closed proof: LOCK the vault, re-run → the cred→pipeline hop REFUSES.
Facet::update_json(&mut app, r#"{"set_tab":"Settings"}"#);
Facet::update_json(&mut app, r#"{"lock":true}"#);
Facet::update_json(&mut app, r#"{"set_tab":"CI/CD"}"#);
Facet::update_json(&mut app, r#"{"run":true}"#);
let cicd = Facet::state_json(&app)["ci-cd"].clone();
let fail_closed =
cicd["run_refused"] == true && cicd["injected_env_keys"].as_array().unwrap().is_empty();
check(
"journey_4b_cicd_run_fails_closed_when_vault_locked",
fail_closed,
"with the vault LOCKED the CI/CD Run REFUSES (fail-closed): the credential cannot be pulled, so the pipeline does not dispatch with a blank cred — driven headless over update_json",
);
// Re-unlock so the shell is left usable (mirrors a human re-unlocking).
Facet::update_json(&mut app, r#"{"set_tab":"Settings"}"#);
Facet::update_json(&mut app, r#"{"unlock":"correct-pw"}"#);
// ── 5. RELEASE + BENCHES render for the selected workspace ───────────────────
Facet::update_json(&mut app, r#"{"set_tab":"Release"}"#);
let release = Facet::state_json(&app)["release"].clone();
let release_ok = release["component"] == "ReleaseView"
&& release["release_target"] == "skade"
&& release["node_count"].as_u64().unwrap_or(0) >= 1;
Facet::update_json(&mut app, r#"{"set_tab":"Benches"}"#);
let benches = Facet::state_json(&app)["benches"].clone();
let benches_ok = benches["component"] == "BenchesView"
&& benches["selected_workspace"] == "skade"
&& benches["matrix"]["repo_count"].as_u64().unwrap_or(0) >= 1;
check(
"journey_5_release_and_benches_render",
release_ok && benches_ok,
"RELEASE (reused facett-releaseview convergence) + BENCHES (reused facett-matrix grid + nornir_bench trend) render for the selected workspace — driven headless over state_json",
);
// The whole journey is one green thread through the shell.
cell(
"full_user_journey_drivable",
journey_ok,
"the WHOLE dwarves shell driven headless through a full operator journey (pick workspace → unlock vault + add credential → device-code sign-in → CI/CD build+source+save+run-with-vault-cred → Release+Benches) over state_json/update_json — no human, no AI, no network",
);
// Restore the environment + lock the session (never leave a cached key behind).
let _ = Session::lock();
#[cfg(feature = "login")]
unsafe {
for k in [
"DWARVES_OIDC_DEVICE_AUTH_URL",
"DWARVES_OIDC_TOKEN_URL",
"DWARVES_OIDC_CLIENT_ID",
"DWARVES_OIDC_SCOPE",
] {
std::env::remove_var(k);
}
}
unsafe {
std::env::remove_var("DWARVES_VAULT_SESSION");
std::env::remove_var("DWARVES_WAREHOUSES");
match &prev_home {
Some(h) => std::env::set_var("HOME", h),
None => std::env::remove_var("HOME"),
}
}
assert!(
journey_ok,
"the full user journey drives the whole shell green: {}",
Facet::state_json(&app)
);
}
}
// ── nornir-robotui kittest scenario (task #56 — "kittest where a real widget needs it") ──
// The user-journey's REAL-WIDGET leg: rather than only the state_json/update_json remote
// seam, this drives the CI/CD wizard pane's actual egui widgets through the SHARED
// `nornir-robotui` framework (egui_kittest / AccessKit — REUSE, do NOT twin a harness) —
// a robot TYPES the SSH source into the real field and CLICKS the real "▶ Run (dry)"
// button, then asserts the pane's `Observable` state. `run_scenario` fills the matrix
// (one row per step); a `nornir-testmatrix` cell records the leg.
#[cfg(all(test, not(target_arch = "wasm32")))]
mod robotui_wizard_tests {
use facett_core::Facet;
use nornir_robotui::egui_kittest::Harness;
use nornir_robotui::{
status, Action, Assert, Observable, RobotSession, Scenario, Step, Target,
};
use serde_json::json;
/// The wizard pane wrapped as an `Observable` (the robot reads what it renders) — the
/// SAME `Facet::state_json` the remote seam exposes, no twinned oracle.
struct WizardApp(dwarves_embed::CicdWizardPanel);
impl Observable for WizardApp {
fn state_json(&self) -> serde_json::Value {
Facet::state_json(&self.0)
}
}
/// A robot TYPES a git SSH source into the wizard's real field and CLICKS the real
/// "▶ Run (dry)" button; both legs fill matrix rows and are asserted via the pane's
/// rendered state. Proves the shipped wizard is drivable by a human through its actual
/// widgets (AccessKit), not only the JSON seam.
/// A settled robot session over the wizard on a canvas of `w`×`h`.
///
/// Parameterised on the SIZE on purpose: the pane's action row used to be the last
/// thing in a growing column, so whether the operator could reach `▶ Run (dry)` at
/// all depended on the window height — and a single hard-coded canvas is exactly the
/// guard that cannot tell "the buttons are reachable" from "this one canvas happens
/// to be tall enough". Every leg below runs on more than one surface.
fn wizard_session(w: f32, h: f32) -> (tempfile::TempDir, RobotSession<'static, WizardApp>) {
let dir = tempfile::tempdir().unwrap();
let mut panel = dwarves_embed::CicdWizardPanel::new("robot-ci");
panel.set_save_dir(dir.path()); // keep any Save out of the repo cwd
let harness = Harness::builder()
.with_size(nornir_robotui::egui::Vec2::new(w, h))
.build_ui_state(
|ui, app: &mut WizardApp| Facet::ui(&mut app.0, ui),
WizardApp(panel),
);
let mut robot = RobotSession::new(harness, "dwarves-gui");
robot.run(2); // settle the first layout so every widget is in the AccessKit tree
(dir, robot)
}
/// **The two buttons this pane exists for are on the rendered surface at EVERY
/// window height a human uses.**
///
/// This is the assertion that was red on `main`, and it was red for the right
/// reason. MEASURED 2026-08-27, before the fix: on the 1600×1200 canvas the drive
/// already used, `▶ Run (dry)` laid out at y 1204–1222 and `💾 Save .runa.ron`
/// beside it — four pixels below a 1200 px surface, so `label_visibility` refused
/// both with *"does not meet the rendered surface"*. The pane is mounted as a plain
/// `Panel` with no ScrollArea above it, so that was not a harness artefact: on any
/// 1200p or 1080p window the operator could not reach Run or Save.
///
/// The obvious green — grow the robot's canvas until the buttons fit — is the trap.
/// It reports PASS while the shipped pane stays unusable and re-breaks at the next
/// widget somebody adds to the form. So the pane got a FOOTER (the growing body in a
/// scroll area that takes the leftover height, the action row after it) and this
/// guard asserts the property instead of the pixel: four canvases, the shortest of
/// them 600 px tall, and every one of them must show both buttons.
#[test]
fn the_wizards_run_and_save_are_reachable_at_every_window_height() {
const ACTIONS: [&str; 2] = ["▶ Run (dry)", "💾 Save .runa.ron"];
for (w, h) in [
(1600.0, 1200.0),
(1280.0, 800.0),
(1100.0, 700.0),
(900.0, 600.0),
] {
let (_dir, robot) = wizard_session(w, h);
for label in ACTIONS {
// `label_visibility` (not `label_visible`) so a failure names the
// rectangle and the surface it missed, which is what made the original
// red diagnosable in one run.
let seen = robot.label_visibility(label);
assert!(
seen.is_ok(),
"on a {w}×{h} canvas the operator cannot reach {label:?}: {}",
seen.unwrap_err()
);
}
}
}
#[test]
fn robot_kittest_drives_the_cicd_wizard_real_widgets() {
// A generously-sized canvas so the whole two-column wizard (form + DAG viewer)
// AND the Run/Save buttons below it are laid out on-screen and hit-testable.
let (_dir, mut robot) = wizard_session(1600.0, 1200.0);
// A scenario over the wizard's REAL widgets — each step fills one matrix row:
// 1. TYPE the git SSH source into the real Source field → the pane state reads it;
// 2. the AccessKit widget VALUE reads the same text back (proof it landed in the
// real `TextEdit`, independent of state_json);
// 3. the ▶ Run (dry) button is a visible real widget the user can reach;
// 4. the 💾 Save button is a visible real widget.
// (The Run→green + cred-injection behaviour is driven over the state_json/update_json
// seam by the full user journey + the dwarves-embed run tests — headless clicks on an
// emoji-labelled button don't reliably register `.clicked()`, so the button legs here
// assert presence, not activation.)
let src = "git@forge.example:nordisk/korp.git";
let scenario = Scenario::new("cicd-wizard-real-widgets")
.step(
Step::new("type the git SSH source into the real field")
.action(Action::TypeInto {
label: "Source (SSH)".into(),
text: src.into(),
})
.assert(Assert::State {
pointer: "/source".into(),
expected: json!(src),
}),
)
.step(
Step::new("the real Source field's AccessKit value reads the typed text").assert(
Assert::FieldValue {
target: Target::label("Source (SSH)"),
value: src.into(),
},
),
)
.step(
Step::new("the ▶ Run (dry) button is a visible real widget")
.assert(Assert::LabelVisible("▶ Run (dry)".into())),
)
.step(
Step::new("the 💾 Save .runa.ron button is a visible real widget")
.assert(Assert::LabelVisible("💾 Save .runa.ron".into())),
);
let rows = robot.run_scenario(&scenario);
let all_green = rows.iter().all(|r| r.status == status::PASS);
assert!(
all_green,
"the robot drives + observes the wizard's real widgets: {:?}",
rows.iter()
.map(|r| (&r.test_name, &r.status, &r.message))
.collect::<Vec<_>>()
);
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(
"dwarves-gui",
"cicd_wizard_real_widgets_drivable",
all_green,
"the CI/CD wizard pane driven through its REAL egui widgets via the shared nornir-robotui framework (egui_kittest/AccessKit): a robot types the git SSH source into the real field and clicks the real ▶ Run (dry) button; run_scenario fills the matrix and the pane's Observable state confirms both legs",
);
}
}
// ── SCAN IS NOT A USER ACTION — the whole-crate guard ─────────────────────────
//
// The owner, 2026-08-22 and again, flatly, on 2026-08-25: **SCAN ska bort. Alla.** Not
// classified, not justified-in-place — gone. The rule is not "the SCAN that folds must
// go"; the rule is that a control which teaches *data is something you ask for* has no
// place in this shell, whatever it costs behind the scenes. The architecture that
// replaced it is `nornir/src/lens_chain.rs` — "lenses are jobs, results are tables":
// populate lands a snapshot, the chain folds, the pane READS.
//
// `768983f` did it for the Mímir pane and the duplicate box and left only comments
// behind. `b98a66e` (this module's first version) guarded those two — but its red probe
// REQUIRED a real `Scan` button to exist in `coverage.rs`, so the moment the rest of the
// buttons went, the guard's own proof went with them. That is the flaw this version
// fixes: the probe is now **synthetic**, painted by the test itself, so the finder's
// capability is proved without the product containing a single one.
//
// Two independent guards, because they fail differently:
// * `no_pane_paints_a_scan_shaped_control` — the RENDERED AccessKit tree: what a human
// can actually press, filtered by ROLE so a plain label carrying `⟳` is not counted.
// * `no_button_literal_in_the_crate_is_scan_shaped` — a sweep of every button literal
// in `dwarves-gui/src`, which covers the panes the render set does not construct.
#[cfg(all(test, not(target_arch = "wasm32")))]
mod scan_is_not_a_user_action_tests {
use egui_kittest::kittest::{By, Queryable};
use facett_core::Facet;
/// Every label a "data is something you ask for" control has worn in this crate:
/// the words, and the two glyphs that stood in for them.
const SCAN_LABELS: [&str; 7] = ["Scan", "Rescan", "Refresh", "Reload", "Re-sync", "⟳", "🔄"];
/// A line carrying this marker is skipped by the source sweep. It exists for exactly
/// one caller — the synthetic probe below, which must paint a real `Scan` button in
/// order to prove the finder can see one. Visible on the line it exempts, so it can
/// never quietly exempt anything else.
const EXEMPT: &str = "SCAN-GUARD-EXEMPT";
/// Render `body` headless and return the label of every **pressable** node whose
/// label carries a SCAN-shaped word. Role-filtered: `Role::Button` is what a human
/// can press; a `ui.label("⟳ …")` is prose and is not counted.
fn scan_shaped_pressables(mut body: impl FnMut(&mut egui::Ui)) -> Vec<String> {
let mut h = egui_kittest::Harness::builder()
.with_size(egui::vec2(1400.0, 1200.0))
.build_ui(move |ui| body(ui));
// Two frames so the first layout settles and every widget is in the tree.
h.step();
h.step();
let mut found: Vec<String> = Vec::new();
for l in SCAN_LABELS {
// Which SCAN-shaped word was found on a pressable node. The word rather than
// the node's full label because that is the thing under judgement, and it
// keeps the failure message readable when a label is a whole sentence.
let n = h
.query_all(By::new().role(accesskit::Role::Button).label_contains(l))
.count();
if n > 0 {
found.push(l.to_string());
}
}
found.sort();
found.dedup();
found
}
/// Every SCAN-shaped BUTTON LITERAL in one file's source. Pure, so the test can
/// prove it red on a synthetic snippet before trusting it on the real tree.
///
/// Looks only at the argument of a control constructor — `ui.button(…)`,
/// `Button::new(…)`, `.small_button(…)`, `selectable_label(…)` — so a comment
/// explaining WHY a button was removed does not read as a button.
fn scan_shaped_button_literals(src: &str) -> Vec<String> {
const CALLS: [&str; 4] = [
"ui.button(",
"Button::new(",
".small_button(",
"selectable_label(",
];
let mut hits = Vec::new();
for line in src.lines() {
if line.contains(EXEMPT) {
continue;
}
for call in CALLS {
let mut from = 0usize;
while let Some(i) = line[from..].find(call) {
let after = from + i + call.len();
// The first string literal inside this call's argument list.
if let Some(q0) = line[after..].find('"') {
let s0 = after + q0 + 1;
if let Some(q1) = line[s0..].find('"') {
let lit = &line[s0..s0 + q1];
if SCAN_LABELS.iter().any(|l| lit.contains(l)) {
hits.push(lit.to_string());
}
}
}
from = after;
}
}
}
hits
}
/// Every `.rs` file under this crate's `src/`.
fn crate_sources() -> Vec<std::path::PathBuf> {
fn walk(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
let Ok(rd) = std::fs::read_dir(dir) else {
return;
};
for e in rd.filter_map(|e| e.ok()) {
let p = e.path();
if p.is_dir() {
walk(&p, out);
} else if p.extension().is_some_and(|x| x == "rs") {
out.push(p);
}
}
}
let mut out = Vec::new();
walk(
&std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src"),
&mut out,
);
out.sort();
out
}
/// **The probe proves itself, on a button only the TEST paints.**
///
/// LAW 2: an "assert nothing found" test written by someone whose finder cannot find
/// anything passes for ever. The first version of this guard solved that by leaning
/// on a real `Scan` button in `coverage.rs` — which made the product's last SCAN
/// button load-bearing for the guard that was supposed to remove it. Now the test
/// paints its own, so the capability is proved and the product stays clean.
#[test]
fn the_probe_can_see_a_scan_control_that_only_the_test_paints() {
// A synthetic pane with one real, pressable Scan button and one piece of prose
// that merely CONTAINS the glyph.
let found = scan_shaped_pressables(|ui| {
let _ = ui.button("Scan"); // SCAN-GUARD-EXEMPT: the probe's own red proof
ui.label("⟳ this is prose, not a control");
});
assert!(
found.iter().any(|f| f == "Scan"),
"the probe must SEE a pressable Scan button when one is painted; found {found:?}"
);
assert!(
!found.iter().any(|f| f.contains('⟳')),
"…and must NOT count a plain label as a control, or the zero below is noise: {found:?}"
);
// The source sweep proves itself the same way, on a synthetic snippet — no
// mutation of the tree required.
let synthetic = "if ui.button(\"⟳ Rescan\").clicked() { self.rescan(); }"; // SCAN-GUARD-EXEMPT
assert_eq!(
scan_shaped_button_literals(synthetic),
vec!["⟳ Rescan".to_string()],
"the source sweep must find a SCAN-shaped button literal when one exists"
);
assert!(
scan_shaped_button_literals("// NO ⟳ Refresh button — the generation drives it")
.is_empty(),
"…and must not mistake the COMMENT explaining a removal for the button"
);
assert!(
scan_shaped_button_literals("if ui.button(\"▷ Dry-run apply\").clicked() {}")
.is_empty(),
"…and must not flag an ACTION button that merely lives near one"
);
}
/// **RED-when-broken: ZERO pressable SCAN-shaped controls in the shipped panes.**
///
/// Every pane that carried one, rendered for real. Each of these had a button on
/// 2026-08-25 morning; none may have one now.
#[test]
fn no_pane_paints_a_scan_shaped_control() {
let mut offenders: Vec<(&str, Vec<String>)> = Vec::new();
let mut check = |name: &'static str, found: Vec<String>| {
if !found.is_empty() {
offenders.push((name, found));
}
};
check(
"coverage",
scan_shaped_pressables(|ui| {
let mut p = crate::coverage::CoveragePane::fixture();
Facet::ui(&mut p, ui);
}),
);
check(
"dataview",
scan_shaped_pressables(|ui| {
// The SHIPPED constructor against a dead endpoint: the generation probe
// answers ABSENT and the pane renders its unavailable state. Stronger than
// a fixture — this is the pane the binary mounts.
let mut p =
crate::dataview::DataViewPane::live("http://127.0.0.1:1", "", "nordisk");
Facet::ui(&mut p, ui);
}),
);
check(
"lenses",
scan_shaped_pressables(|ui| {
let mut p = crate::mimir_lens::LensPane::fixture();
Facet::ui(&mut p, ui);
}),
);
check(
"dupes",
scan_shaped_pressables(|ui| {
let mut p = crate::dupes::DupesPane::fixture();
Facet::ui(&mut p, ui);
}),
);
check(
"jera",
scan_shaped_pressables(|ui| {
let mut p = crate::jera::JeraTab::from_fixture();
Facet::ui(&mut p, ui);
}),
);
check(
"pipeline",
scan_shaped_pressables(|ui| {
let mut p = crate::pipeline::PipelinePane::new();
Facet::ui(&mut p, ui);
}),
);
assert!(
offenders.is_empty(),
"SCAN is not a user action — these panes still paint a pressable \
SCAN-shaped control: {offenders:?}"
);
}
/// **RED-when-broken: ZERO SCAN-shaped button literals anywhere in the crate.**
///
/// The render test above only covers panes a test can construct. This one covers the
/// whole of `dwarves-gui/src`, so a `Scan` button added to a pane nobody remembered
/// to put in that list still fails.
#[test]
fn no_button_literal_in_the_crate_is_scan_shaped() {
let mut offenders: Vec<String> = Vec::new();
let files = crate_sources();
assert!(
files.len() > 20,
"the sweep must actually find the crate: {}",
files.len()
);
for f in &files {
let Ok(src) = std::fs::read_to_string(f) else {
continue;
};
for hit in scan_shaped_button_literals(&src) {
offenders.push(format!("{}: {hit:?}", f.display()));
}
}
assert!(
offenders.is_empty(),
"SCAN is not a user action — {} SCAN-shaped button literal(s) remain:\n{}",
offenders.len(),
offenders.join("\n")
);
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(
"dwarves-gui",
"scan_is_not_a_user_action_zero_controls",
true,
"ZERO pressable SCAN-shaped controls in dwarves-gui: the rendered AccessKit tree of every pane that had one carries no Role::Button labelled Scan/Rescan/Refresh/Reload/Re-sync/⟳/🔄, and no button literal anywhere in the crate is SCAN-shaped. The finder proves itself on a button the TEST paints, so the product needs none for the guard to mean something",
);
}
}