# 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 default `kind()` to `""`).
* `<n>` — a small integer that **MUST NEVER repeat within the same component**, so the
full `facet-<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):
```rust
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):
```rust
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:
```rust
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` + the `FacetError` struct + logs + the About /
Diagnostics popup — so robot tests + diagnostics key off it regardless of profile.
* `state_json` ALWAYS 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 every `code` filled 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
* **`Errors` tab** (`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 reports `Severity::Info` (it displays codes as a *catalog*; it is not itself in a
failure state) and renders via `FacetError::render_demo` (neutral a11y atoms) so it
never trips the Robot-UI HARD GATE or the deck error-atom net.
* **`Manual` tab** (`facett_manual::ManualPane::embedded`) — the real in-app manual.
* **About / Diagnostics popup** (the `legend` toggle) — the git-hash build stamp + a
codeberg source link per component + the registry summary. Mirrored in
`state_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.