//! **facett-core::errcode** — the STABLE, UNIQUE **UI error-code** scheme.
//!
//! Today a facett UI error is caught by a robot test only by COMPARING the rendered
//! string — fragile: reword the message and the test goes blind. This module gives
//! every facett UI part a **stable, unique error code** of the form
//! **`facet-<component>-<n>`** so tests + consumers react to the CODE, not a matched
//! string. "Not only the string tells it's wrong."
//!
//! ## The scheme
//! * `<component>` is the facet component **slug** — the same string a
//! [`Facet::kind`](crate::Facet::kind) returns (`map`, `graph3d`, `geomap`,
//! `systemmap`, `population`, …).
//! * `<n>` is a small integer that **MUST NEVER repeat within the same component**,
//! so the full `facet-<component>-<n>` code is **globally unique** across ALL of
//! facett. The [`REGISTRY`] is the single source of truth and
//! [`tests`](self#tests) FAILS the build if any code repeats.
//!
//! ## What an error carries
//! A raised [`FacetError`] carries `{ code, component, n, message, codeberg_url }`:
//! the **code** (stable id), the **message** (the unchanged human-readable string —
//! the UX is untouched), and the **codeberg source URL** for the component so a
//! reviewer can jump straight to where it is raised.
//!
//! ## How it renders
//! Every error still emits its VISIBLE human message (in the warm failure [`RED`]).
//! In a NON-release (**debug**) build the CODE is *also* drawn on-screen in **clear
//! [`PINK`]** ([`code_color`]); in a release build the code is subdued on-screen but
//! is ALWAYS present in `state_json` + the [`FacetError`] struct + logs, so robot
//! tests + diagnostics key off it regardless of build profile.
//!
//! ## Ergonomics
//! A pane raises an error with the [`facet_err!`](crate::facet_err) macro:
//! ```ignore
//! // facet-map-1 with a runtime-formatted message; the code + codeberg URL come
//! // from the REGISTRY entry.
//! let e = facett_core::facet_err!(map, 1, "tiles failed to load: {err}");
//! e.render(ui); // message (red) + code (pink in debug)
//! json["error"] = e.to_json(); // code lands in state_json
//! ```
//!
//! ## The guard
//! [`REGISTRY`] is a plain `const` table; the uniqueness [`tests`](self#tests) walk it
//! and FAIL if (a) any `code` repeats globally, (b) any `n` repeats within a
//! component, or (c) a `code` string does not equal `facet-<component>-<n>`. That is
//! the compile-/test-time guard that makes the scheme trustworthy.
use egui::{Color32, Response, Ui};
use crate::look::{PINK, RED};
/// The **codeberg** repo the source URLs point into (matches the workspace
/// `repository` field). Source files browse at `<BASE>/src/branch/main/<path>`.
pub const CODEBERG_REPO: &str = "https://codeberg.org/nordisk/facett";
/// The branch the source links resolve against.
pub const CODEBERG_BRANCH: &str = "main";
/// **How a CONSUMER must react** to a raised facett UI error (Phase 2).
///
/// The shared reaction vocabulary: facett declares the CANONICAL reaction per code
/// (in [`REGISTRY`]) so every consumer — korp, nornir, dwarves, holger — agrees on
/// what a given failure *means*, and a consumer's robot test asserts the REACTION
/// (never a string match):
///
/// ```ignore
/// assert_eq!(reaction_for("korp/cases:facet-map-2"), Reaction::EmptyState);
/// ```
///
/// A consumer may still choose a stricter local behaviour, but the canonical reaction
/// is what the reaction-matrix tests pin — that is what makes ~150 (code × mount)
/// assertions table-driven instead of hand-written.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Reaction {
/// **Transient load failure — RETRY.** The source/tile/fetch did not complete;
/// the consumer re-attempts (with backoff) and shows a retry affordance.
Retry,
/// **No data — show an HONEST EMPTY STATE.** Not a failure of the machinery: the
/// query/selection genuinely has nothing. The consumer must NOT show a blank pane;
/// it shows "nothing here" plus what would fill it.
EmptyState,
/// **Backend connection is down — offer RECONNECT.** The consumer surfaces the
/// disconnected state and a reconnect action; it must not pretend to show data.
Reconnect,
/// **Degraded but FUNCTIONAL — inform, do not alarm.** A fast path (GPU) is
/// absent and a slower correct path took over. The consumer notes it in
/// diagnostics; the UI keeps working.
Fallback,
/// **Internal/compute failure — DIAGNOSE.** A solver diverged, a layout did not
/// converge, a raster produced nothing. The consumer records a diagnostic with the
/// code and surfaces a "something is wrong here" state to the operator.
Diagnose,
}
impl Reaction {
/// The stable machine token (state_json / test tables / the matrix slide).
pub fn token(self) -> &'static str {
match self {
Reaction::Retry => "retry",
Reaction::EmptyState => "empty_state",
Reaction::Reconnect => "reconnect",
Reaction::Fallback => "fallback",
Reaction::Diagnose => "diagnose",
}
}
/// A one-line description of what the consumer must actually do.
pub fn describe(self) -> &'static str {
match self {
Reaction::Retry => "re-attempt the load (with backoff) + show a retry affordance",
Reaction::EmptyState => "show an honest empty state — never a blank pane",
Reaction::Reconnect => "surface the disconnected state + a reconnect action",
Reaction::Fallback => "note the degrade in diagnostics; keep working on the slow path",
Reaction::Diagnose => "record a diagnostic with the code + surface the fault to the operator",
}
}
/// Parse a reaction token (the inverse of [`token`](Self::token)).
pub fn parse(s: &str) -> Option<Reaction> {
match s.trim().to_ascii_lowercase().as_str() {
"retry" => Some(Reaction::Retry),
"empty_state" | "empty" => Some(Reaction::EmptyState),
"reconnect" => Some(Reaction::Reconnect),
"fallback" => Some(Reaction::Fallback),
"diagnose" => Some(Reaction::Diagnose),
_ => None,
}
}
}
/// One row of the canonical error-code catalog — the single source of truth for a
/// `facet-<component>-<n>` code. The [`REGISTRY`] is a `const` slice of these; the
/// uniqueness [`tests`](self#tests) walk it, and the facett-demo **error-code matrix
/// slide** renders it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ErrCode {
/// The facet component **slug** (a [`Facet::kind`](crate::Facet::kind), e.g. `map`).
pub component: &'static str,
/// The per-component number. MUST be unique within `component`.
pub n: u32,
/// The full stable code — MUST equal `facet-<component>-<n>` (guarded by a test).
pub code: &'static str,
/// The canonical human-readable message (the UX string; a raise may format extra
/// runtime detail on top of it).
pub message: &'static str,
/// The component's crate directory under the repo root (e.g. `facett-map`).
pub crate_dir: &'static str,
/// The source file (repo-relative to `crate_dir`) where the code is raised
/// (e.g. `src/lib.rs`).
pub src_file: &'static str,
/// **Phase-2** column: which CONSUMER mount points react to this code (a
/// comma-separated `app/mount` list, e.g. `korp/infra, korp/cases`). `""` means no
/// consumer mounts this component today (facett-demo always renders it).
pub consumer: &'static str,
/// **Phase-2**: the CANONICAL [`Reaction`] a consumer must take for this code.
/// This is what the (code × mount) reaction-matrix tests assert.
pub reaction: Reaction,
}
impl ErrCode {
/// The codeberg source URL for this code —
/// `https://codeberg.org/nordisk/facett/src/branch/main/<crate_dir>/<src_file>`.
pub fn codeberg_url(&self) -> String {
format!(
"{CODEBERG_REPO}/src/branch/{CODEBERG_BRANCH}/{}/{}",
self.crate_dir, self.src_file
)
}
}
/// The canonical error-code **REGISTRY** — THE LIST. Every `facet-<component>-<n>`
/// code in all of facett is declared here exactly once. Adding a UI error site =
/// adding a row here (and raising it with [`facet_err!`](crate::facet_err)). The
/// uniqueness guard in [`tests`](self#tests) fails the build on any repeat.
///
/// Ordered by component, then `n`. Keep it that way — it is read top-to-bottom as the
/// demo's error-code matrix slide and exported to the registry markdown.
///
/// ## APPEND-ONLY (frozen catalog)
/// Each `<n>` is assigned ONCE per component and **never renumbered or reused** — like
/// a real error catalog. A retired code's row stays (mark it in the message); a new
/// error gets the **next free integer** for that component. Consumers and the Phase-2
/// reaction tests pin these strings, so stability is the entire point.
pub const REGISTRY: &[ErrCode] = &[
// ── map — facett-map (2D vessel/vector basemap) ───────────────────────────────
// (Canonical messages avoid the robot-UI ERROR_MARKERS stems so the demo's
// matrix slide can render the catalog without tripping the atom-walk gate.)
ec("map", 1, "map basemap tiles did not load", "facett-map", "src/lib.rs", "korp/infra, korp/cases", Reaction::Retry),
ec("map", 2, "map has no vessels / no positions to plot", "facett-map", "src/lib.rs", "korp/infra, korp/cases", Reaction::EmptyState),
ec("map", 3, "map GPU renderer not present — CPU painter fallback", "facett-map", "src/gpu.rs", "korp/infra, korp/cases", Reaction::Fallback),
// map-4 is map-2's MISSING TWIN. `map-2` says "there are no vessels"; until now a
// vessel source that could not be READ produced the same empty plot and the same
// EmptyState reaction. Same picture, opposite repair: one is correct and final, the
// other is a retryable read failure. (`consumer` stays "" — no mount reacts to it
// yet, and claiming one is the fiction the Phase-2 guard exists to catch.)
ec("map", 4, "map vessel/position source could not be read — the plot is empty because the READ failed, not because there are no vessels", "facett-map", "src/lib.rs", "", Reaction::Retry),
// ── map3d — facett-map3d (3D extruded OSM city) ───────────────────────────────
ec("map3d", 1, "3D map has no ways/buildings to extrude", "facett-map3d", "src/lib.rs", "korp/infra", Reaction::EmptyState),
ec("map3d", 2, "3D map GPU depth renderer not present — CPU depth-sort fallback", "facett-map3d", "src/gpu.rs", "korp/infra", Reaction::Fallback),
ec("map3d", 3, "3D map terrain/relief tile did not load", "facett-map3d", "src/lib.rs", "korp/infra", Reaction::Retry),
// map3d-4 is map3d-1's missing twin: nothing extruded because the READ failed.
ec("map3d", 4, "3D map ways/buildings source could not be read — nothing is extruded because the READ failed, not because the area is empty", "facett-map3d", "src/lib.rs", "", Reaction::Retry),
// ── geomap — facett-geomap (slippy OSM / geo scatter) ─────────────────────────
ec("geomap", 1, "geomap GeoParquet source did not load", "facett-geomap", "src/lib.rs", "korp/infra", Reaction::Retry),
ec("geomap", 2, "geomap has no points / hotspots in view", "facett-geomap", "src/lib.rs", "korp/infra", Reaction::EmptyState),
ec("geomap", 3, "geomap tile fetch did not complete", "facett-geomap", "src/osm.rs", "korp/infra", Reaction::Retry),
// ── osm — facett-osm (the WKB reader every OSM geometry enters facett through) ─
// These two are the READER's half of the map3d-4/map-4 family: a pane that drew
// nothing needs to be able to say whether the source was empty or whether the
// DECODER refused it. Before them, `decode_wkb` handled types 1/2/3 and returned
// an empty Vec — indistinguishable from a clean read — for MultiPoint (4),
// MultiLineString (5), MultiPolygon (6) and GeometryCollection (7). Those four
// are decoded now; osm-1 covers whatever is left (curves, TIN, a corrupt type
// word), which cannot even be SKIPPED because its size is unknown.
ec("osm", 1, "OSM WKB geometry of an unsupported type was refused — those features are missing because the DECODER could not read them, not because the area is empty", "facett-osm", "src/wkb.rs", "", Reaction::Diagnose),
// osm-2 is the byte-length invariant. A decoder that is one field out of phase
// does not run off the end; it lands somewhere plausible and returns confident
// nonsense (an earlier reader read closed ways as LineString, 4 bytes out of
// phase, and reported a 60.48% figure that was really the polygon share).
// Comparing the length the header IMPLIES with the length the blob HAS is what
// makes that loud instead of believable.
ec("osm", 2, "OSM WKB blob disagrees with its own header — it ended inside a promised field, opened with a bad byte-order byte, or left bytes over after the geometry", "facett-osm", "src/wkb.rs", "", Reaction::Diagnose),
// ── graph3d — facett-graph3d (3D force cloud) ─────────────────────────────────
ec("graph3d", 1, "graph3d has no nodes to render", "facett-graph3d", "src/lib.rs", "korp/infra, nornir/viz", Reaction::EmptyState),
ec("graph3d", 2, "graph3d GPU instanced-cloud renderer not present — CPU painter fallback", "facett-graph3d", "src/graph_gpu.rs", "korp/infra, nornir/viz", Reaction::Fallback),
ec("graph3d", 3, "graph3d force layout did not converge (degenerate positions)", "facett-graph3d", "src/lib.rs", "korp/infra, nornir/viz", Reaction::Diagnose),
// graph3d-4 is graph3d-1's missing twin. graphview already had this split
// (graphview-1 empty vs graphview-3 source did not load); graph3d did not.
ec("graph3d", 4, "graph3d node source could not be read — the cloud is empty because the READ failed, not because the graph has no nodes", "facett-graph3d", "src/lib.rs", "", Reaction::Retry),
// ── graphview — facett-graphview (consolidated L0 render engine) ──────────────
ec("graphview", 1, "graphview has an empty scene — nothing to lay out", "facett-graphview", "src/metro.rs", "nornir/viz, dwarves/funnel", Reaction::EmptyState),
ec("graphview", 2, "graphview L0 CPU raster produced no pixels", "facett-graphview", "src/lib.rs", "nornir/viz, dwarves/funnel", Reaction::Diagnose),
ec("graphview", 3, "graphview graph source did not load", "facett-graphview", "src/falkor.rs", "nornir/viz, dwarves/funnel", Reaction::Retry),
// ── population — facett-demo pop_tab over knut-popsim (the Sverige showcase) ───
ec("population", 1, "population query returned no people at this scale/filter", "facett-demo", "src/pop_tab.rs", "", Reaction::EmptyState),
ec("population", 2, "population model could not build the sample", "facett-demo", "src/pop_model.rs", "", Reaction::Diagnose),
// ── systemmap — facett-graph3d SystemMap + facett-cfd fluid (🫧 System Map) ────
ec("systemmap", 1, "system map has no components/pipes to draw", "facett-graph3d", "src/pipes.rs", "", Reaction::EmptyState),
ec("systemmap", 2, "system map fluid solver diverged (non-finite state)", "facett-cfd", "src/lib.rs", "", Reaction::Diagnose),
// systemmap-3 is systemmap-1's missing twin: no components drawn because the
// topology could not be READ, not because the system has none.
ec("systemmap", 3, "system map topology source could not be read — nothing is drawn because the READ failed, not because the system is empty", "facett-graph3d", "src/pipes.rs", "", Reaction::Retry),
// ── syschart — facett-syschart (peers + badges system chart) ──────────────────
ec("syschart", 1, "system chart has no peers to show", "facett-syschart", "src/lib.rs", "holger/mannequin", Reaction::EmptyState),
ec("syschart", 2, "system chart peer reported an error badge", "facett-syschart", "src/lib.rs", "holger/mannequin", Reaction::Diagnose),
// syschart-3 is syschart-1's missing twin, and the most dangerous of the set: "no
// peers" and "I could not read the peer roster" both render as an empty chart, and
// the second means the monitoring surface itself is blind.
ec("syschart", 3, "system chart peer roster could not be read — the chart is empty because the READ failed, not because there are no peers", "facett-syschart", "src/lib.rs", "", Reaction::Retry),
// ── cfd — facett-cfd (gatling fluid engine) ───────────────────────────────────
ec("cfd", 1, "fluid step produced a non-finite cell (NaN/Inf)", "facett-cfd", "src/lib.rs", "", Reaction::Diagnose),
ec("cfd", 2, "fluid pipe network is empty — nothing to simulate", "facett-cfd", "src/lib.rs", "", Reaction::EmptyState),
// ── korp — facett-korp (korp caseworker mode host) ────────────────────────────
ec("korp", 1, "korp backend connection not established", "facett-korp", "src/lib.rs", "korp/cases, korp/analysis", Reaction::Reconnect),
ec("korp", 2, "korp case view has no cases to display", "facett-korp", "src/search.rs", "korp/cases, korp/analysis", Reaction::EmptyState),
// korp-3 is korp-2's missing twin — and it is the exact shape of the ⚒ Build Thing
// bug that recurred four times: the connection is UP, the query FAILED, and the
// pane showed "no cases". `korp-1` does not cover it (that is "not connected").
ec("korp", 3, "korp case query failed on a LIVE connection — the list is empty because the QUERY failed, not because there are no cases", "facett-korp", "src/search.rs", "", Reaction::Retry),
// korp-4/5/6 split what `korp-1` ("connection not established") lumped together.
// Refused, timed-out and rejected look identical to a user and need three different
// repairs: start the backend / wait or raise the deadline / fix the credential.
// Retrying a REJECTED identity forever is the classic wrong reaction, which is why
// korp-6 is Diagnose rather than Reconnect. korp-1 remains for a genuinely unknown
// cause, so nothing that already pins it changes meaning.
ec("korp", 4, "korp backend REFUSED the connection — nothing is listening at the endpoint", "facett-korp", "src/lib.rs", "", Reaction::Reconnect),
ec("korp", 5, "korp backend did not answer in time — it is listening but not responding", "facett-korp", "src/lib.rs", "", Reaction::Retry),
ec("korp", 6, "korp backend REJECTED the identity — connected, but not authorised; retrying will not help", "facett-korp", "src/lib.rs", "", Reaction::Diagnose),
// ── demo — facett-demo showcase host (the error-code showcase itself) ─────────
ec("demo", 1, "example error: a triggered demo condition (showcase)", "facett-demo", "src/errcode_tab.rs", "", Reaction::Diagnose),
ec("demo", 2, "example note: a triggered demo degrade (showcase)", "facett-demo", "src/errcode_tab.rs", "", Reaction::Fallback),
// ── gpu — facett-core adapter policy (PROCESS-wide, not a pane) ───────────────
// GFX_V2 Decision 0 dropped `Backends::GL`, so a host without a WebGPU-class
// device now gets NOTHING where it previously got a degraded WebGL picture.
// These two codes are what makes that absence STATED rather than a blank
// window. Reaction is Diagnose, never Fallback: the whole point of Decision 0
// is that there is no silent soft-render lane to fall back to. (The per-pane
// `*-GPU renderer not present — CPU painter fallback` codes above are a
// DIFFERENT thing — a working CPU lane took over. Do not conflate them.)
// `consumer` is "" because no mount reacts to these yet; declaring mounts that
// do not wire them is exactly the fiction the Phase-2 anti-fiction guard exists
// to catch.
//
// Their two states are NOT the same, and the registry's `status` column said
// `reserved` for both:
//
// * `facet-gpu-1` IS raised today. `facett_wgpu_options`' `native_adapter_selector`
// calls `gpu_unavailable_for` -> `classify_unavailable` when no adapter can be
// chosen, records the typed value and surfaces its Display (code + remedy) as the
// eframe bring-up error; `facet-wrapped`'s wasm canvas raises the same code.
// * `facet-gpu-2` is **still genuinely reserved, and cannot fire.** The policy never
// refuses a non-empty adapter list — it *flags* software/BMC and picks them as a
// last resort (see `GpuUnavailable::AllRejected` and the invariant test
// `the_policy_never_refuses_a_non_empty_adapter_list`). Kept for the day the
// policy is made strict, at which point that test goes red and points here.
ec("gpu", 1, "no GPU adapter enumerated — facett has no render lane", "facett-core", "src/render/gpu/adapter_wgpu.rs", "", Reaction::Diagnose),
ec("gpu", 2, "every GPU adapter was rejected (software rasteriser / management console)", "facett-core", "src/render/gpu/adapter_wgpu.rs", "", Reaction::Diagnose),
];
/// `const fn` row builder — fills `code` from `component`+`n` is NOT possible in a
/// `const` (no `format!`), so the `code` literal is passed by the [`REGISTRY`] rows
/// via the `ec!`-shaped helper below; here we take the pre-formatted parts. Defaults
/// `consumer` to `""` (Phase-2 fills it). Keeping this `const` lets [`REGISTRY`] stay
/// a compile-time table.
const fn ec(
component: &'static str,
n: u32,
message: &'static str,
crate_dir: &'static str,
src_file: &'static str,
consumer: &'static str,
reaction: Reaction,
) -> ErrCode {
ErrCode { component, n, code: "", message, crate_dir, src_file, consumer, reaction }
}
// NOTE: `ec()` leaves `code` = "" because a `const fn` cannot `format!`. The public
// accessor [`registry`] fills each row's `code` on first access from a `LazyLock`, so
// callers always see the well-formed `facet-<component>-<n>`. The uniqueness test
// asserts the derived code is what we expect.
use std::sync::LazyLock;
/// The [`REGISTRY`] with every `code` filled in (`facet-<component>-<n>`), computed
/// once. Callers should use THIS (via [`registry`]) rather than the raw `REGISTRY`
/// const, whose `code` fields are empty placeholders (a `const fn` cannot `format!`).
static FILLED: LazyLock<Vec<ErrCode>> = LazyLock::new(|| {
REGISTRY
.iter()
.map(|e| ErrCode { code: leak_code(e.component, e.n), ..*e })
.collect()
});
/// The canonical registry, every `code` resolved. Read this, not the raw `REGISTRY`.
pub fn registry() -> &'static [ErrCode] {
&FILLED
}
/// Format+leak `facet-<component>-<n>` into a `&'static str` (done once per row, at
/// registry init — a bounded, one-time leak, not a hot path).
fn leak_code(component: &str, n: u32) -> &'static str {
Box::leak(format!("facet-{component}-{n}").into_boxed_str())
}
/// Look up a registry row by its full `code` (`facet-map-1`).
pub fn lookup(code: &str) -> Option<&'static ErrCode> {
registry().iter().find(|e| e.code == code)
}
/// Look up a registry row by `(component, n)`.
pub fn lookup_cn(component: &str, n: u32) -> Option<&'static ErrCode> {
registry().iter().find(|e| e.component == component && e.n == n)
}
/// **The canonical [`Reaction`] for a code** — what a consumer MUST do when this
/// facett UI error is raised (Phase 2). Accepts either a bare facett code
/// (`facet-map-2`) or a consumer-MOUNTED code (`korp/cases:facet-map-2`): the mount
/// prefix is stripped, because the reaction is a property of the CODE, and the mount
/// only says *where* it happened. Unknown codes yield `None`.
///
/// ```ignore
/// assert_eq!(reaction_for("korp/cases:facet-map-2"), Some(Reaction::EmptyState));
/// assert_eq!(reaction_for("facet-map-2"), Some(Reaction::EmptyState));
/// ```
pub fn reaction_for(code: &str) -> Option<Reaction> {
lookup(base_code(code)).map(|e| e.reaction)
}
/// Strip a consumer mount-prefix from a (possibly mounted) code:
/// `"korp/cases:facet-map-2"` → `"facet-map-2"`. A bare code passes through.
pub fn base_code(code: &str) -> &str {
match code.rsplit_once(':') {
Some((_, base)) => base,
None => code,
}
}
/// The consumer mount-prefix of a mounted code (`"korp/cases:facet-map-2"` →
/// `Some("korp/cases")`), or `None` for a bare facett code.
pub fn mount_of(code: &str) -> Option<&str> {
code.rsplit_once(':').map(|(m, _)| m)
}
/// Every consumer mount point declared for a code, parsed from its registry
/// `consumer` column (`"korp/infra, korp/cases"` → `["korp/infra", "korp/cases"]`).
/// Empty when no consumer mounts this component today.
pub fn mounts_for(code: &str) -> Vec<&'static str> {
lookup(base_code(code))
.map(|e| {
e.consumer
.split(',')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.collect()
})
.unwrap_or_default()
}
/// The full **reaction matrix**: every `(mounted_code, base_code, mount, reaction)`
/// the consumers must implement — the table the ~150 reaction tests are generated
/// from. Codes with no consumer mount are omitted (facett-demo renders them, but no
/// app reacts yet).
pub fn reaction_matrix() -> Vec<(String, &'static str, &'static str, Reaction)> {
let mut out = Vec::new();
for e in registry() {
for m in mounts_for(e.code) {
out.push((format!("{m}:{}", e.code), e.code, m, e.reaction));
}
}
out
}
/// The codeberg source URL for a `code`, or the repo root if the code is unknown.
pub fn codeberg_url(code: &str) -> String {
lookup(code).map(|e| e.codeberg_url()).unwrap_or_else(|| CODEBERG_REPO.to_string())
}
/// Every distinct component slug present in the registry, in first-seen order.
pub fn components() -> Vec<&'static str> {
let mut out: Vec<&'static str> = Vec::new();
for e in registry() {
if !out.contains(&e.component) {
out.push(e.component);
}
}
out
}
/// The codes that are RAISED at a real pane site today (as opposed to catalogued +
/// reserved for a load/GPU-fallback path that has no explicit failure branch yet).
/// Kept here — beside the registry — so the markdown export and the demo can both
/// mark the distinction, and so adding a wiring is a one-line change next to the row.
pub const WIRED: &[&str] = &[
"facet-map-2",
"facet-map3d-1",
"facet-geomap-2",
"facet-graph3d-1",
"facet-graphview-1",
"facet-population-1",
"facet-systemmap-1",
"facet-syschart-1",
"facet-cfd-2",
"facet-korp-1",
"facet-korp-2",
"facet-demo-1",
"facet-demo-2",
];
/// Is this code raised at a real pane site today? (See [`WIRED`].)
pub fn is_wired(code: &str) -> bool {
WIRED.contains(&base_code(code))
}
/// **Render THE LIST as markdown** from the compiled [`REGISTRY`] — the single source
/// of truth. Used by the `errcode_registry_md` bin to (re)generate
/// `.nornir/facett-error-codes-registry.md`; a freshness test compares the committed
/// copy against this so the export can never drift.
pub fn registry_markdown() -> String {
use std::fmt::Write as _;
let reg = registry();
let matrix = reaction_matrix();
let wired = reg.iter().filter(|e| is_wired(e.code)).count();
let reserved = reg.len() - wired;
let mut s = String::new();
s.push_str("# facett UI error-code registry — THE LIST\n\n");
s.push_str(
"<!-- GENERATED FILE — do not hand-edit.\n Regenerate: cargo run -p facett-core --bin errcode_registry_md > .nornir/facett-error-codes-registry.md\n Source of truth: facett-core/src/errcode.rs (errcode::REGISTRY). -->\n\n",
);
s.push_str(
"The canonical catalog of every `facet-<component>-<n>` code. **Append-only / frozen:**\nan `<n>` is assigned once and never reused. The uniqueness guard (`errcode` tests)\nfails the build on any repeat.\n\n",
);
let _ = writeln!(
s,
"* **Codeberg base:** `{CODEBERG_REPO}/src/branch/{CODEBERG_BRANCH}/`"
);
s.push_str("* **status:** `wired` = raised at a real pane site today (a11y code + `Severity::Error` when empty, `error_code` in `state_json`, a `severity()` override, pink code on painter panes). `reserved` = catalogued, unique, guard-checked and shown in the matrix slide, awaiting an explicit failure branch on that load / GPU-fallback path.\n");
s.push_str("* **reaction (Phase 2):** what a CONSUMER must do when the code is raised — the vocabulary is `facett_core::errcode::Reaction`. This is what the (code × mount) reaction tests assert, never a string match.\n");
s.push_str("* **consumer mounts:** the `app/surface` mount points that react. A consumer namespaces the code with its mount, e.g. `korp/cases:facet-map-2` vs `korp/infra:facet-map-2` — same facet code, distinct mount points a robot test pins apart.\n\n");
let _ = writeln!(
s,
"**Totals:** {} components, {} codes — **{wired} wired**, **{reserved} reserved**. Reaction matrix: **{} (code × mount) pairs**.\n",
components().len(),
reg.len(),
matrix.len()
);
s.push_str("| component | code | visible message | codeberg source | status | reaction (Phase 2) | consumer mounts |\n");
s.push_str("|-----------|------|-----------------|-----------------|--------|--------------------|------------------|\n");
for e in reg {
let mounts = if e.consumer.is_empty() { "— (none today)".to_string() } else { format!("`{}`", e.consumer) };
let _ = writeln!(
s,
"| {} | `{}` | {} | {}/{} | {} | `{}` — {} | {} |",
e.component,
e.code,
e.message,
e.crate_dir,
e.src_file,
if is_wired(e.code) { "wired" } else { "reserved" },
e.reaction.token(),
e.reaction.describe(),
mounts,
);
}
s.push_str("\n## The reaction MATRIX (Phase 2 test table)\n\n");
s.push_str(
"Every row below is one `(code × mount)` pair the consumer reaction tests assert.\nThe test harness is TABLE-DRIVEN over `errcode::reaction_matrix()`, so adding a code\n(or a mount) adds rows — never a hand-written copy.\n\n",
);
s.push_str("| mounted code | base code | mount | expected reaction |\n");
s.push_str("|--------------|-----------|-------|-------------------|\n");
for (mounted, base, mount, reaction) in &matrix {
let _ = writeln!(s, "| `{mounted}` | `{base}` | `{mount}` | `{}` |", reaction.token());
}
s.push_str("\n## How a consumer reacts\n\n");
s.push_str("```rust\n// The consumer catches a facett error, namespaces it with ITS mount, records the\n// MOUNTED code into its own diagnostics, and reacts per the canonical table.\nlet reaction = errcode::reaction_for(&mounted_code).expect(\"a registered code\");\nmatch reaction {\n Reaction::EmptyState => show_honest_empty_state(),\n Reaction::Retry => retry_with_backoff(),\n Reaction::Reconnect => surface_reconnect_action(),\n Reaction::Fallback => note_degrade_and_keep_working(),\n Reaction::Diagnose => record_diagnostic_and_surface_fault(),\n}\n// The robot test pins the REACTION for the MOUNTED code — never a matched string:\nassert_eq!(reaction_for(\"korp/cases:facet-map-2\"), Some(Reaction::EmptyState));\n```\n");
s
}
/// **Export the whole catalog as JSON** — the machine-readable twin of
/// [`registry_markdown`], and the seam that keeps the two demos in LOCKSTEP (LAW #2):
/// the native egui demo reads the compiled [`REGISTRY`] directly, and the
/// Python/Streamlit demo reads THIS json (generated into
/// `py/facett_demo_python/errcodes.json`), so both render the SAME catalog from the
/// SAME source of truth. A freshness test fails if the committed json drifts.
pub fn registry_json() -> serde_json::Value {
let codes: Vec<serde_json::Value> = registry()
.iter()
.map(|e| {
serde_json::json!({
"component": e.component,
"n": e.n,
"code": e.code,
"message": e.message,
"crate": e.crate_dir,
"src_file": e.src_file,
"codeberg_url": e.codeberg_url(),
"status": if is_wired(e.code) { "wired" } else { "reserved" },
"reaction": e.reaction.token(),
"reaction_describes": e.reaction.describe(),
"consumer": e.consumer,
})
})
.collect();
let matrix: Vec<serde_json::Value> = reaction_matrix()
.into_iter()
.map(|(mounted, base, mount, reaction)| {
serde_json::json!({
"mounted_code": mounted,
"code": base,
"mount": mount,
"reaction": reaction.token(),
})
})
.collect();
serde_json::json!({
"_generated": "GENERATED from facett-core errcode::REGISTRY — do not hand-edit. \
Regenerate: cargo run -p facett-core --bin errcode_registry_json > py/facett_demo_python/errcodes.json",
"scheme": "facet-<component>-<n>",
"codeberg_repo": CODEBERG_REPO,
"codeberg_branch": CODEBERG_BRANCH,
"component_count": components().len(),
"code_count": registry().len(),
"wired_count": registry().iter().filter(|e| is_wired(e.code)).count(),
"reaction_pairs": matrix.len(),
"components": components(),
"reactions": [
{ "token": Reaction::Retry.token(), "describes": Reaction::Retry.describe() },
{ "token": Reaction::EmptyState.token(), "describes": Reaction::EmptyState.describe() },
{ "token": Reaction::Reconnect.token(), "describes": Reaction::Reconnect.describe() },
{ "token": Reaction::Fallback.token(), "describes": Reaction::Fallback.describe() },
{ "token": Reaction::Diagnose.token(), "describes": Reaction::Diagnose.describe() },
],
"codes": codes,
"reaction_matrix": matrix,
})
}
// ── The reusable CONSUMER side (Phase 2) ──────────────────────────────────────
// korp integrates facett errors into its own typed `DiagCode` ring (it had one
// already). The other consumers — nornir, dwarves, holger — do NOT need a bespoke
// module each: they declare their mounts and record through this shared recorder,
// so "how a consumer reacts" is implemented ONCE and every app agrees by construction.
/// A **consumer mount point** — `<app>/<surface>` (e.g. `nornir/viz`,
/// `dwarves/funnel`, `holger/mannequin`). The prefix a consumer prepends to a facett
/// code so the same component mounted twice yields two distinguishable mounted codes.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ConsumerMount {
/// The consuming application (`nornir`, `dwarves`, `holger`, `korp`).
pub app: &'static str,
/// The surface within that app (`viz`, `funnel`, `mannequin`, …).
pub surface: &'static str,
/// The source file that actually mounts the facett component — the ANTI-FICTION
/// anchor: a consumer's guard test asserts this file exists and really references
/// the component, so a declared mount can never be fiction.
pub site: &'static str,
}
impl ConsumerMount {
/// The mount prefix (`"nornir/viz"`).
pub fn prefix(&self) -> String {
format!("{}/{}", self.app, self.surface)
}
}
/// One recorded facett UI error at a consumer mount — what the consumer surfaces into
/// its own `state_json` / diagnostics.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MountedRecord {
/// The mount-namespaced code (`nornir/viz:facet-graph3d-1`).
pub mounted_code: String,
/// The base facett code (`facet-graph3d-1`) — frozen, facett's.
pub code: &'static str,
/// The mount prefix (`nornir/viz`).
pub mount: String,
/// The canonical reaction the consumer took.
pub reaction: Reaction,
/// The visible human message (UX unchanged).
pub message: String,
}
impl MountedRecord {
pub fn to_json(&self) -> serde_json::Value {
serde_json::json!({
"mounted_code": self.mounted_code,
"code": self.code,
"mount": self.mount,
"reaction": self.reaction.token(),
"message": self.message,
})
}
}
/// A bounded **recorder** a consumer keeps so raised facett UI errors land in its own
/// observable state. This is the shared implementation of the Phase-2 consumer
/// contract: namespace with the mount, resolve the canonical reaction, record the
/// MOUNTED code, hand the reaction back for the app to act on.
///
/// ```ignore
/// let mut rec = FacetRecorder::default();
/// let reaction = rec.record(NORNIR_VIZ, &err); // → Reaction::EmptyState
/// json["facet_errors"] = rec.state_json(); // the mounted code rides in state
/// ```
#[derive(Debug, Default, Clone)]
pub struct FacetRecorder {
entries: Vec<MountedRecord>,
total: u64,
}
/// How many records a [`FacetRecorder`] keeps (newest-last; older ones evict).
pub const RECORDER_CAP: usize = 64;
impl FacetRecorder {
pub fn new() -> Self {
Self::default()
}
/// **Record a facett UI error at `mount` and return the REACTION to take.** An
/// unregistered code yields [`Reaction::Diagnose`] — never a silent pass.
pub fn record(&mut self, mount: ConsumerMount, err: &FacetError) -> Reaction {
let reaction = reaction_for(err.code).unwrap_or(Reaction::Diagnose);
let prefix = mount.prefix();
self.total += 1;
self.entries.push(MountedRecord {
mounted_code: format!("{prefix}:{}", err.code),
code: err.code,
mount: prefix,
reaction,
message: err.message.clone(),
});
if self.entries.len() > RECORDER_CAP {
self.entries.remove(0);
}
reaction
}
/// Every recorded entry (newest last).
pub fn entries(&self) -> &[MountedRecord] {
&self.entries
}
/// Total ever recorded (survives eviction).
pub fn total(&self) -> u64 {
self.total
}
/// Whether a given mounted code was recorded.
pub fn saw(&self, mounted_code: &str) -> bool {
self.entries.iter().any(|e| e.mounted_code == mounted_code)
}
pub fn clear(&mut self) {
self.entries.clear();
}
/// The observable block a consumer folds into its `state_json` — a robot reads the
/// CODE and the REACTION, never a matched string.
pub fn state_json(&self) -> serde_json::Value {
serde_json::json!({
"count": self.entries.len(),
"total": self.total,
"entries": self.entries.iter().map(|e| e.to_json()).collect::<Vec<_>>(),
})
}
}
/// The reaction-matrix rows belonging to ONE app (`"nornir"`, `"dwarves"`, …) —
/// what that consumer's table-driven test suite must satisfy. Derived from the
/// shared [`reaction_matrix`], so declaring a mount in the registry automatically
/// creates the app's obligations.
pub fn matrix_for_app(app: &str) -> Vec<(String, &'static str, &'static str, Reaction)> {
let want = format!("{app}/");
reaction_matrix()
.into_iter()
.filter(|(_, _, mount, _)| mount.starts_with(&want))
.collect()
}
/// The **on-screen colour** for an error CODE: the clear [`PINK`] in a NON-release
/// (debug) build, a subdued muted pink in release (the code is still present in
/// `state_json` + logs — only its on-screen prominence drops). This is decision (2)
/// of the design.
pub fn code_color() -> Color32 {
if cfg!(debug_assertions) {
PINK
} else {
// Subdued: same hue, low prominence, so a release build does not shout the
// developer-facing code but a reviewer can still spot it.
Color32::from_rgba_unmultiplied(PINK.r(), PINK.g(), PINK.b(), 90)
}
}
/// A **raised facett UI error** — the typed carrier of a `facet-<component>-<n>`
/// code + its human message + the codeberg source URL. Build it with
/// [`facet_err!`](crate::facet_err). It renders the message (red) plus the code
/// (pink in debug), folds into `state_json` via [`FacetError::to_json`], and reports
/// [`Severity::Error`](crate::Severity::Error).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FacetError {
/// The stable code (`facet-<component>-<n>`).
pub code: &'static str,
/// The component slug.
pub component: &'static str,
/// The per-component number.
pub n: u32,
/// The human-readable message (UX unchanged; may carry runtime detail).
pub message: String,
/// The codeberg source URL for the component (resolved from the [`REGISTRY`]).
pub codeberg_url: String,
}
impl FacetError {
/// Construct from parts — normally called by [`facet_err!`](crate::facet_err),
/// which supplies the compile-time `component`/`n`/`code`. The codeberg URL is
/// resolved from the [`REGISTRY`] (repo root if the code is not yet registered).
pub fn new(component: &'static str, n: u32, code: &'static str, message: impl Into<String>) -> Self {
Self {
code,
component,
n,
message: message.into(),
codeberg_url: codeberg_url(code),
}
}
/// This error's structural [`Severity`](crate::Severity) — always
/// [`Severity::Error`](crate::Severity::Error) (a raised `FacetError` is RED and
/// fails the Robot-UI gate).
pub fn severity(&self) -> crate::Severity {
crate::Severity::Error
}
/// The observable JSON an error site folds into its pane's `state_json` — the
/// CODE (not just the message) so robot tests + consumers key off it:
/// `{ code, component, n, message, codeberg_url, severity: "error" }`.
pub fn to_json(&self) -> serde_json::Value {
serde_json::json!({
"code": self.code,
"component": self.component,
"n": self.n,
"message": self.message,
"codeberg_url": self.codeberg_url,
"severity": "error",
// Phase 2: the canonical REACTION a consumer must take for this code.
"reaction": self.reaction().map(|r| r.token()),
})
}
/// The canonical [`Reaction`] a consumer must take for this error (Phase 2).
pub fn reaction(&self) -> Option<Reaction> {
reaction_for(self.code)
}
/// The a11y [`Semantics`](crate::a11y::Semantics) for this error — a
/// [`Severity::Error`](crate::Severity::Error) atom carrying the code, so the
/// Robot-UI gate reads the code straight off the AccessKit tree.
pub fn semantics(&self) -> crate::a11y::Semantics {
crate::a11y::Semantics::error(format!("{} [{}]", self.message, self.code)).error_code(self.code)
}
/// **Namespace this error with a CONSUMER mount-prefix.** A facett component can
/// be mounted in MULTIPLE places in a consuming app (korp mounts `facet-map` in
/// both its *cases* and *analysis* modes), so the consumer prepends ITS OWN
/// mount-prefix when it surfaces/reacts to a facett error:
/// `korp/cases:facet-map-2` vs `korp/analysis:facet-map-2` — the SAME base facet
/// code `facet-map-2`, two distinct 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 (this is Phase-1 API that Phase-2 wires in).
pub fn with_consumer_prefix(&self, prefix: impl Into<String>) -> MountedFacetError {
MountedFacetError { prefix: prefix.into(), inner: self.clone() }
}
/// **Render** the error into `ui`: the human message in the warm failure [`RED`],
/// then the CODE in [`code_color`] (clear pink in debug, subdued in release), and
/// an AccessKit node carrying [`Severity::Error`](crate::Severity::Error) + the
/// code. UX is unchanged (the message is always shown); the pink code is the
/// added diagnostic stripe.
pub fn render(&self, ui: &mut Ui) -> Response {
use egui::RichText;
let resp = ui
.horizontal(|ui| {
ui.label(RichText::new(&self.message).color(RED).strong());
// The pink CODE chip — the stable diagnostic id.
ui.label(
RichText::new(format!(" {} ", self.code))
.monospace()
.strong()
.color(Color32::WHITE)
.background_color(code_color()),
)
})
.response;
// Ride the code into the AccessKit tree as an Error atom so a headless robot
// (and the severity fold) sees the code, not the pixels.
resp.widget_info(|| self.semantics().widget_info());
resp
}
/// **Render for a SHOWCASE / catalog** (the facett-demo error-code slide) — the
/// SAME visual (message + pink code chip) as [`render`](Self::render), but the
/// a11y atom is a neutral [`Severity::Info`](crate::Severity::Info) `Label`, NOT
/// an `Error`. This is what a demonstration surface (which shows error codes on
/// purpose, as a catalog) uses so it does NOT trip the Robot-UI HARD GATE or the
/// deck error-atom net. A LIVE pane raising a real failure uses [`render`](Self::render).
pub fn render_demo(&self, ui: &mut Ui) -> Response {
use egui::RichText;
let resp = ui
.horizontal(|ui| {
ui.label(RichText::new(&self.message).color(RED));
ui.label(
RichText::new(format!(" {} ", self.code))
.monospace()
.strong()
.color(Color32::WHITE)
.background_color(code_color()),
)
})
.response;
// Neutral (Info) label atom carrying the code — no Error severity.
let code = self.code;
resp.widget_info(|| crate::a11y::Semantics::new(egui::WidgetType::Label, format!("code {code}")).widget_info());
resp
}
}
/// **Paint an error CODE into a `Painter`** (for the many facett panes that draw
/// their empty/error hint with `ui.painter()` rather than widgets). Draws the code
/// in [`code_color`] (clear pink in debug, subdued in release) centred at `pos` and
/// returns the drawn text rect. UX-additive: the caller still paints its human hint;
/// this is the pink diagnostic stripe beneath it. Pair it with an a11y
/// [`Semantics`](crate::a11y::Semantics)`::error(..).error_code(code)` on the pane so
/// the code also rides the AccessKit tree.
pub fn paint_code(painter: &egui::Painter, pos: egui::Pos2, code: &str) -> egui::Rect {
painter.text(
pos,
egui::Align2::CENTER_CENTER,
code,
egui::FontId::monospace(11.0),
code_color(),
)
}
/// A facett [`FacetError`] **mounted by a consumer** — the base facett code
/// namespaced with the consumer's own mount-prefix (design decision 3). The base
/// code is untouched; `mounted_code()` is `"<prefix>:<code>"`.
///
/// ```ignore
/// let e = facett_core::facet_err!(map, 2, "no positions to plot");
/// let m = e.with_consumer_prefix("korp/cases");
/// assert_eq!(m.mounted_code(), "korp/cases:facet-map-2");
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MountedFacetError {
/// The consumer's mount-prefix (e.g. `korp/cases`).
pub prefix: String,
/// The underlying facett error (base code unchanged).
pub inner: FacetError,
}
impl MountedFacetError {
/// The namespaced code — `"<prefix>:<facet-component-n>"`.
pub fn mounted_code(&self) -> String {
format!("{}:{}", self.prefix, self.inner.code)
}
/// The observable JSON — carries BOTH the base facett `code` and the consumer's
/// `mounted_code`, plus the (unchanged) message + codeberg URL. This is the shape
/// a Phase-2 consumer robot test pins per mount point.
pub fn to_json(&self) -> serde_json::Value {
let mut j = self.inner.to_json();
if let serde_json::Value::Object(m) = &mut j {
m.insert("mount".into(), serde_json::Value::String(self.prefix.clone()));
m.insert("mounted_code".into(), serde_json::Value::String(self.mounted_code()));
}
j
}
}
/// **Raise a facett UI error** — the ergonomic front door for the error-code scheme.
///
/// `facet_err!(component, n, "message {with} {fmt}", ...)` builds a [`FacetError`]
/// whose `code` is `facet-<component>-<n>` (formed at compile time from the literal
/// `component` ident + `n` literal) and whose message is the (optionally formatted)
/// string. The codeberg URL is resolved from the [`REGISTRY`].
///
/// ```ignore
/// let e = facett_core::facet_err!(map, 1, "tiles failed to load");
/// assert_eq!(e.code, "facet-map-1");
/// ```
/// **Is `facet-<component>-<n>` a REGISTERED code?** — answerable at COMPILE TIME.
///
/// This is what closes the hole `facet_err!` was born with. The macro `concat!`s its code
/// from literals, so it could always name a code the REGISTRY had never heard of; the
/// mistake compiled cleanly and only surfaced later as a `lookup` returning `None` and a
/// `reaction_for` answering `None` — a code with no defined reaction, which is worse than
/// no code at all.
///
/// A `const fn` can walk the `const REGISTRY`, so the macro can assert membership in a
/// `const` context and turn that class of mistake into a **compile error**. This is the
/// same property nornir and holger get from their `codes!` macro (a code that is not
/// declared cannot be named), reached without renaming the catalog or touching a single
/// call site.
pub const fn is_registered(component: &str, n: u32) -> bool {
let mut i = 0;
while i < REGISTRY.len() {
if REGISTRY[i].n == n && const_str_eq(REGISTRY[i].component, component) {
return true;
}
i += 1;
}
false
}
/// `&str` equality in a `const` context (`==` is not const on `str`).
const fn const_str_eq(a: &str, b: &str) -> bool {
let (a, b) = (a.as_bytes(), b.as_bytes());
if a.len() != b.len() {
return false;
}
let mut i = 0;
while i < a.len() {
if a[i] != b[i] {
return false;
}
i += 1;
}
true
}
#[macro_export]
macro_rules! facet_err {
($comp:ident, $n:literal, $($msg:tt)*) => {{
// COMPILE-TIME membership check: naming a code the REGISTRY does not declare is
// now a build failure, not a runtime surprise. See `errcode::is_registered`.
const _: () = ::core::assert!(
$crate::errcode::is_registered(stringify!($comp), $n),
"facet_err! names a code that is NOT in errcode::REGISTRY — add the row first",
);
$crate::errcode::FacetError::new(
stringify!($comp),
$n,
::core::concat!("facet-", stringify!($comp), "-", stringify!($n)),
::std::format!($($msg)*),
)
}};
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::{HashMap, HashSet};
/// THE GUARD: no `code` repeats globally, and no `n` repeats within a component.
#[test]
fn codes_are_globally_unique_and_per_component_n_unique() {
let reg = registry();
let mut seen_codes: HashSet<&str> = HashSet::new();
let mut per_component: HashMap<&str, HashSet<u32>> = HashMap::new();
for e in reg {
assert!(
seen_codes.insert(e.code),
"DUPLICATE error code {:?} — every facet-<component>-<n> must be globally unique",
e.code
);
let ns = per_component.entry(e.component).or_default();
assert!(
ns.insert(e.n),
"DUPLICATE n={} within component {:?} — n must never repeat inside a component",
e.n,
e.component
);
}
assert!(!reg.is_empty(), "the registry must not be empty");
}
/// Every `code` string MUST equal `facet-<component>-<n>`.
#[test]
fn code_strings_are_well_formed() {
for e in registry() {
assert_eq!(e.code, format!("facet-{}-{}", e.component, e.n), "malformed code");
assert!(!e.message.is_empty(), "{} has an empty message", e.code);
assert!(!e.crate_dir.is_empty() && !e.src_file.is_empty(), "{} missing source", e.code);
}
}
/// The codeberg URL resolves to the component's source file.
#[test]
fn codeberg_urls_resolve() {
let e = lookup("facet-map-1").expect("facet-map-1 registered");
assert_eq!(
e.codeberg_url(),
"https://codeberg.org/nordisk/facett/src/branch/main/facett-map/src/lib.rs"
);
assert_eq!(codeberg_url("facet-map-1"), e.codeberg_url());
// An unknown code falls back to the repo root, never panics.
assert_eq!(codeberg_url("facet-nope-99"), CODEBERG_REPO);
}
/// The macro forms the code at compile time and resolves the URL.
#[test]
fn macro_builds_a_well_formed_error() {
let e = crate::facet_err!(map, 1, "tiles failed: {}", 42);
assert_eq!(e.code, "facet-map-1");
assert_eq!(e.component, "map");
assert_eq!(e.n, 1);
assert_eq!(e.message, "tiles failed: 42");
assert_eq!(e.severity(), crate::Severity::Error);
let j = e.to_json();
assert_eq!(j["code"], "facet-map-1");
assert_eq!(j["severity"], "error");
assert!(j["codeberg_url"].as_str().unwrap().contains("facett-map"));
}
#[test]
fn consumer_prefix_namespaces_the_code() {
let e = crate::facet_err!(map, 2, "no positions to plot");
let m = e.with_consumer_prefix("korp/cases");
assert_eq!(m.mounted_code(), "korp/cases:facet-map-2");
// The base code is untouched.
assert_eq!(m.inner.code, "facet-map-2");
let j = m.to_json();
assert_eq!(j["code"], "facet-map-2", "base code preserved");
assert_eq!(j["mounted_code"], "korp/cases:facet-map-2");
assert_eq!(j["mount"], "korp/cases");
// Same base code, two mount points, distinct mounted codes.
let a = e.with_consumer_prefix("korp/analysis");
assert_ne!(m.mounted_code(), a.mounted_code());
assert_eq!(m.inner.code, a.inner.code);
}
/// Phase 2: every code carries a canonical reaction, and the reaction is a
/// property of the CODE — a mounted code resolves to the same reaction as its base.
#[test]
fn every_code_has_a_canonical_reaction_independent_of_mount() {
for e in registry() {
assert_eq!(reaction_for(e.code), Some(e.reaction), "{} reaction", e.code);
// The mount prefix must NOT change the reaction.
let mounted = format!("korp/cases:{}", e.code);
assert_eq!(reaction_for(&mounted), Some(e.reaction), "{mounted} reaction");
assert_eq!(base_code(&mounted), e.code);
assert_eq!(mount_of(&mounted), Some("korp/cases"));
}
assert_eq!(reaction_for("facet-nope-99"), None, "unknown code has no reaction");
assert_eq!(base_code("facet-map-2"), "facet-map-2", "a bare code passes through");
assert_eq!(mount_of("facet-map-2"), None);
}
/// The empty-data codes must all react with an HONEST EMPTY STATE, and the
/// GPU-absent codes with a FALLBACK — the two rules that matter most for "the
/// consumer must never show a blank pane".
#[test]
fn reaction_assignments_are_semantically_right() {
assert_eq!(reaction_for("facet-map-2"), Some(Reaction::EmptyState));
assert_eq!(reaction_for("facet-graph3d-1"), Some(Reaction::EmptyState));
assert_eq!(reaction_for("facet-syschart-1"), Some(Reaction::EmptyState));
assert_eq!(reaction_for("facet-korp-2"), Some(Reaction::EmptyState));
// GPU-absent lanes degrade, they do not alarm.
assert_eq!(reaction_for("facet-map-3"), Some(Reaction::Fallback));
assert_eq!(reaction_for("facet-map3d-2"), Some(Reaction::Fallback));
assert_eq!(reaction_for("facet-graph3d-2"), Some(Reaction::Fallback));
// Transient loads retry.
assert_eq!(reaction_for("facet-map-1"), Some(Reaction::Retry));
assert_eq!(reaction_for("facet-geomap-1"), Some(Reaction::Retry));
// A dead backend offers reconnect.
assert_eq!(reaction_for("facet-korp-1"), Some(Reaction::Reconnect));
// Compute faults are diagnosed.
assert_eq!(reaction_for("facet-cfd-1"), Some(Reaction::Diagnose));
assert_eq!(reaction_for("facet-systemmap-2"), Some(Reaction::Diagnose));
}
/// The reaction MATRIX — the table the consumer robot tests are generated from.
/// Every row is a real (mount × code) pair with a canonical reaction, every
/// mounted code is unique, and the matrix is non-trivial.
#[test]
fn reaction_matrix_is_well_formed_and_covers_the_mounted_codes() {
let matrix = reaction_matrix();
assert!(!matrix.is_empty(), "the reaction matrix must not be empty");
let mut seen = std::collections::HashSet::new();
for (mounted, base, mount, reaction) in &matrix {
assert!(seen.insert(mounted.clone()), "duplicate matrix row {mounted}");
assert_eq!(mounted, &format!("{mount}:{base}"));
assert_eq!(reaction_for(mounted), Some(*reaction));
assert!(mount.contains('/'), "a mount is `<app>/<surface>`, got {mount}");
assert!(lookup(base).is_some(), "{base} is a registered code");
}
// Every code that declares consumer mounts appears in the matrix.
for e in registry() {
let n = mounts_for(e.code).len();
let rows = matrix.iter().filter(|(_, b, _, _)| *b == e.code).count();
assert_eq!(rows, n, "{} contributes one row per declared mount", e.code);
}
eprintln!("\n══ facett reaction MATRIX ══\n {} (code × mount) pairs\n", matrix.len());
}
/// A raised error carries its reaction into `state_json` (the consumer reads the
/// CODE + the REACTION as data, never a matched string).
#[test]
fn raised_error_json_carries_the_reaction() {
let e = crate::facet_err!(map, 2, "no positions to plot");
let j = e.to_json();
assert_eq!(j["code"], "facet-map-2");
assert_eq!(j["reaction"], "empty_state");
assert_eq!(e.reaction(), Some(Reaction::EmptyState));
// The mounted form keeps both the base code and the reaction.
let m = e.with_consumer_prefix("korp/cases");
let mj = m.to_json();
assert_eq!(mj["mounted_code"], "korp/cases:facet-map-2");
assert_eq!(mj["reaction"], "empty_state");
}
#[test]
fn reaction_tokens_roundtrip() {
for r in [
Reaction::Retry,
Reaction::EmptyState,
Reaction::Reconnect,
Reaction::Fallback,
Reaction::Diagnose,
] {
assert_eq!(Reaction::parse(r.token()), Some(r), "roundtrip {r:?}");
assert!(!r.describe().is_empty());
}
assert_eq!(Reaction::parse("bogus"), None);
}
/// The shared consumer recorder: namespaces, reacts, records the MOUNTED code.
#[test]
fn facet_recorder_namespaces_reacts_and_records() {
const VIZ: ConsumerMount =
ConsumerMount { app: "nornir", surface: "viz", site: "src/autonom/facett_probe.rs" };
assert_eq!(VIZ.prefix(), "nornir/viz");
let mut rec = FacetRecorder::new();
let err = crate::facet_err!(graph3d, 1, "no nodes to render");
let reaction = rec.record(VIZ, &err);
assert_eq!(reaction, Reaction::EmptyState, "an empty cloud shows an empty state");
assert!(rec.saw("nornir/viz:facet-graph3d-1"), "the MOUNTED code was recorded");
let j = rec.state_json();
assert_eq!(j["count"], 1);
assert_eq!(j["entries"][0]["mounted_code"], "nornir/viz:facet-graph3d-1");
assert_eq!(j["entries"][0]["code"], "facet-graph3d-1", "base code preserved");
assert_eq!(j["entries"][0]["reaction"], "empty_state");
// The human message survives — the UX is unchanged.
assert_eq!(j["entries"][0]["message"], "no nodes to render");
}
/// An unregistered code is DIAGNOSED, never silently ignored.
#[test]
fn facet_recorder_diagnoses_an_unknown_code() {
const M: ConsumerMount = ConsumerMount { app: "dwarves", surface: "funnel", site: "x.rs" };
let mut rec = FacetRecorder::new();
let bogus = FacetError::new("nope", 99, "facet-nope-99", "invented");
assert_eq!(rec.record(M, &bogus), Reaction::Diagnose);
assert!(rec.saw("dwarves/funnel:facet-nope-99"));
}
/// The recorder is bounded — a hot error loop cannot grow it without bound.
#[test]
fn facet_recorder_is_bounded() {
const M: ConsumerMount = ConsumerMount { app: "holger", surface: "mannequin", site: "x.rs" };
let mut rec = FacetRecorder::new();
let err = crate::facet_err!(syschart, 1, "no peers to show");
for _ in 0..(RECORDER_CAP + 20) {
rec.record(M, &err);
}
assert_eq!(rec.entries().len(), RECORDER_CAP, "the ring is capped");
assert_eq!(rec.total(), (RECORDER_CAP + 20) as u64, "the total is honest");
}
/// Per-app matrix slicing — each consumer's obligations come from the shared table.
#[test]
fn matrix_for_app_slices_the_shared_table() {
for app in ["korp", "nornir", "dwarves", "holger"] {
let rows = matrix_for_app(app);
assert!(!rows.is_empty(), "{app} mounts at least one facett component");
for (mounted, base, mount, reaction) in &rows {
assert!(mount.starts_with(&format!("{app}/")), "{mounted} belongs to {app}");
assert_eq!(mounted, &format!("{mount}:{base}"));
assert_eq!(reaction_for(mounted), Some(*reaction));
}
}
// Every row of the whole matrix belongs to exactly one app slice.
let total: usize = ["korp", "nornir", "dwarves", "holger"]
.iter()
.map(|a| matrix_for_app(a).len())
.sum();
assert_eq!(total, reaction_matrix().len(), "the app slices partition the matrix");
}
#[test]
fn components_are_discoverable() {
let comps = components();
for want in ["map", "map3d", "geomap", "graph3d", "graphview", "cfd", "korp"] {
assert!(comps.contains(&want), "component {want} missing from registry");
}
}
}