Gunnar

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

* `<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.