facett UI error-code scheme — design
Status: Phase 1 (facett side) — implemented in facett-core::errcode. Anchor: every facett UI part carries a STABLE, UNIQUE error code so tests + consumers react to the code, not a matched string. "Not only the string tells it's wrong."
The problem
Today a facett UI error is caught by a robot test only by COMPARING the rendered output STRING (nornir_robotui::ERROR_MARKERS — "Failed to load", "unavailable", …). That is fragile: reword the message and the test goes blind; two different failures that render the same words are indistinguishable; a consumer cannot key a reaction off "which exact failure was this".
The scheme
Every facett UI part (each pane / component / distinct error site) gets a stable unique code of the form:
facet-<component>-<n>
<component>— the facet component slug (map,map3d,geomap,graph3d,graphview,population,systemmap,syschart,cfd,korp,demo, …). This is the component identity; it matches a [Facet::kind] where one is set, but the scheme owns the slug regardless (many facets defaultkind()to"").<n>— a small integer that MUST NEVER repeat within the same component, so the fullfacet-<component>-<n>string is globally unique across ALL of facett.
FROZEN + append-only (decision, Rickard)
Each <n> is assigned once per component and never renumbered or reused — like a real error catalog. Components are stable ("we seldom remove facet components"), so codes are permanent. A retired site keeps its row (annotate the message); a new error site gets the next free integer for that component. Consumers and the Phase-2 reaction tests (~150) pin these strings — stability is the entire point.
The error type — DUAL channels, always both
facett_core::errcode::FacetError carries BOTH report channels, always present (not one-or-the-other):
pub struct FacetError {
pub code: &'static str, // (b) the stable CODE — "facet-map-1"
pub component: &'static str, // the slug
pub n: u32,
pub message: String, // (a) the VISIBLE human string (unchanged UX)
pub codeberg_url: String, // source link for the component
}
Raised ergonomically with the facet_err! macro (the code is formed at compile time from the component ident + n literal; the message may be runtime-formatted):
let e = facett_core::facet_err!(map, 1, "tiles did not load: {err}");
assert_eq!(e.code, "facet-map-1");
e.render(ui); // message (RED) + code (PINK in debug) + a11y Error atom
json["error"] = e.to_json(); // { code, component, n, message, codeberg_url, severity }
Consumer mount-prefix (Phase-1 API, Phase-2 wiring)
A facett component can be mounted in multiple places in a consuming app, so the consumer prepends ITS OWN mount-prefix when it surfaces/reacts to a facett error:
let e = facet_err!(map, 2, "no positions to plot");
e.with_consumer_prefix("korp/cases").mounted_code(); // "korp/cases:facet-map-2"
e.with_consumer_prefix("korp/analysis").mounted_code(); // "korp/analysis:facet-map-2"
Same base facet code facet-map-2, two mount points a robot test can pin apart. The base facett code stays facet-<component>-<n>; the prefix is additive and applied only by the consumer. MountedFacetError::to_json() carries BOTH the base code and the mounted_code.
Rendering — visible string always; pink code by build profile
- The human message is ALWAYS shown (warm failure
look::color::RED) — the UX is unchanged. - The code is drawn on-screen in CLEAR PINK (
look::color::PINK,#FF40B4) in a NON-release (debug) build —errcode::code_color(). - In a release build the on-screen code is subdued (low-alpha pink), but the code is ALWAYS present in
state_json+ theFacetErrorstruct + logs + the About / Diagnostics popup — so robot tests + diagnostics key off it regardless of profile. state_jsonALWAYS carries the code in both profiles.
The code also rides the AccessKit tree: a11y::Semantics gained an additive code: Option<&'static str> field (.error_code(code)), and FacetError::render attaches a Severity::Error atom carrying the code, so a headless robot reads the CODE off the tree, not the pixels.
The registry — THE LIST + the uniqueness guard
errcode::REGISTRY is a single const table (the single source of truth); every facet-<component>-<n> is declared there exactly once with its canonical message, crate + source file (→ codeberg URL), and a Phase-2 consumer column. Reading the registry:
errcode::registry()— the rows with everycodefilled in.errcode::lookup(code)/lookup_cn(component, n).errcode::codeberg_url(code)—https://codeberg.org/nordisk/facett/src/branch/main/<crate>/<src_file>.errcode::components()— the distinct slugs.
The guard (errcode tests, run every build): FAIL if (a) any code repeats globally, (b) any n repeats within a component, or (c) a code string is not exactly facet-<component>-<n>. That is what makes the scheme trustworthy — a duplicate code is a red build, not a silent collision.
facett-demo showcase
Errorstab (errcode_tab::ErrCodeTab) — triggers example errors so the pink codes render (debug), shows the version + git-hash stamp, a codeberg link per component, the consumer mount-prefix demo, and the error-code MATRIX slide over the whole registry (component → code → message → codeberg → Phase-2 consumer). The tab reportsSeverity::Info(it displays codes as a catalog; it is not itself in a failure state) and renders viaFacetError::render_demo(neutral a11y atoms) so it never trips the Robot-UI HARD GATE or the deck error-atom net.Manualtab (facett_manual::ManualPane::embedded) — the real in-app manual.- About / Diagnostics popup (the
legendtoggle) — the git-hash build stamp + a codeberg source link per component + the registry summary. Mirrored instate_json.about.
Why messages avoid the old markers
The canonical registry messages are worded to avoid the five nornir_robotui::ERROR_MARKERS stems (Failed to load, is not served, unavailable, RPC: status:, panicked) so the demo's matrix slide can render the whole catalog on-screen without the legacy substring-scan gate going RED. Real panes still key their gate off the structural Severity + the code, not the string.
Wired sites (Phase 1 complete)
Every code below is RAISED at its real pane site (a11y error_code + Severity::Error when empty, error_code in state_json, a severity() override, and the pink paint_code where the pane is painter-based):
| code | pane | file |
facet-map-2 |
MapView empty geo | facett-map/src/lib.rs |
facet-graph3d-1 |
Graph3D no nodes | facett-graph3d/src/lib.rs |
facet-systemmap-1 |
SystemMap empty | facett-graph3d/src/pipes.rs |
facet-geomap-2 |
GeoMap no geo data | facett-geomap/src/lib.rs |
facet-cfd-2 |
fluid needs ≥2 snapshots | facett-cfd/src/lib.rs |
facet-korp-1 |
IngestPane no collector | facett-korp/src/lib.rs |
facet-korp-2 |
SearchPane no results | facett-korp/src/search.rs |
facet-graphview-1 |
MetroView no lines | facett-graphview/src/metro.rs |
facet-syschart-1 |
SystemChart no peers | facett-syschart/src/lib.rs |
facet-map3d-1 |
Map3D no ways to extrude | facett-map3d/src/lib.rs |
facet-population-1 |
Sverige empty query | facett-demo/src/pop_tab.rs |
facet-demo-1/2 |
showcase examples | facett-demo/src/errcode_tab.rs |
The remaining registry rows are RESERVED, catalogued codes for load/GPU-fallback paths (map-1/3, map3d-2/3, geomap-1/3, graph3d-2/3, graphview-2/3, population-2, systemmap-2, syschart-2, cfd-1) — declared, unique, guard-checked and rendered in the matrix slide, to be raised as those paths gain explicit failure branches. The catalog is append-only, so a reserved n is never re-assigned.
Phase 2 (NOT this phase)
Consumer adoption (korp / nornir / dwarves / holger) + the ~150 reaction tests: each consumer mount reacts to a facet-<component>-<n> code (namespaced with its mount-prefix) and a test pins the reaction. The consumer column in the registry is where those land. Phase 2 is dispatched after this LIST is reviewed.