Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

.nornir/facett-error-codes-design.md

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>

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 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:

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

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.