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

nordisk/facett

facett-core/src/errcode.rs

//! **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, source_url }`:
//! the **code** (stable id), the **message** (the unchanged human-readable string —
//! the UX is untouched), and the **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 + source 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 repository's public file pages on gunnar.rs, which the source URLs point into (matches the workspace
/// `repository` field). Source files browse at `<BASE>/blob/main/<path>`.
pub const SOURCE_REPO: &str = SOURCE.base;
/// The branch the source links resolve against.
pub const SOURCE_BRANCH: &str = SOURCE.branch;
/// facett's repository, for the ONE link builder every registry shares (`facett-source-link`,
/// RA2-14; dwarves and korp name theirs the same way).
pub const SOURCE: facett_source_link::SourceRepo = facett_source_link::SourceRepo::new("https://gunnar.rs/nordisk/facett");

/// **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 source URL (gunnar.rs's public file page) for this code —
    /// `https://gunnar.rs/nordisk/facett/blob/main/<crate_dir>/<src_file>`.
    pub fn source_url(&self) -> String {
        SOURCE.blob(&format!("{}/{}", 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/mod.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/osm2d.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),
    // ── graph — facett-graph (the 2-D scene+layout GraphView) ────────────────────────
    // Registered 2026-09-29 so the pane has a dev-id: before it, facett-demo's first
    // pane numbered as `⟨unregistered⟩ graph` in the navgraph (navgraph-design §9).
    ec("graph", 1, "graph has an empty scene — no nodes to draw", "facett-graph", "src/lib.rs", "", Reaction::EmptyState),
    // graph-2 is graph-1's twin: nothing drawn because the scene source could not be READ.
    ec("graph", 2, "graph scene source could not be read — the pane is empty because the READ failed, not because the graph has no nodes", "facett-graph", "src/lib.rs", "", Reaction::Retry),
    // ── 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/lib.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),
    // graph3d-5 — the HOST placement (Layout3D::Given) does not cover every node.
    // Not an error: the grid fallback still places everything, so the picture is
    // honest. It is a FALLBACK, and it is a code rather than a silence because a
    // partly-given cloud would otherwise draw the unplaced tail at the origin and
    // that reads as data. Same law korp's geographic layout runs on.
    ec("graph3d", 5, "graph3d host placement (layout `given`) does not cover every node — the grid lattice drew it instead", "facett-graph3d", "src/lib.rs", "korp/infra", Reaction::Fallback),
    // ── 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 source URL (gunnar.rs's public file page) for a `code`, or the repo root if the code is unknown.
pub fn source_url(code: &str) -> String {
    lookup(code).map(|e| e.source_url()).unwrap_or_else(|| SOURCE.link_or_repo(None))
}

/// 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-graph-1",
    "facet-graph3d-1",
    "facet-graph3d-5",
    "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,
        "* **Source base:** `{}`",
        SOURCE.blob_base()
    );
    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 | 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/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,
                "source_url": e.source_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/errcodes.json",
        "scheme": "facet-<component>-<n>",
        "source_repo": SOURCE_REPO,
        "source_branch": SOURCE_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 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 source URL (gunnar.rs's public file page) for the component (resolved from the [`REGISTRY`]).
    pub source_url: String,
}

impl FacetError {
    /// Construct from parts — normally called by [`facet_err!`](crate::facet_err),
    /// which supplies the compile-time `component`/`n`/`code`. The source 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(),
            source_url: source_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, source_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,
            "source_url": self.source_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 + source 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 source 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 {
    /// ★ Owner decision 2026-10-26 (item 3): every error code's source link is gunnar.rs's
    /// public file page, `https://gunnar.rs/nordisk/facett/blob/main/<path>`, for a file that
    /// is in this repository.
    #[test]
    fn every_error_codes_source_link_is_a_gunnar_rs_file_page() {
        let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
        for e in registry() {
            let path = format!("{}/{}", e.crate_dir, e.src_file);
            assert_eq!(e.source_url(), format!("https://gunnar.rs/nordisk/facett/blob/main/{path}"), "{}", e.code);
            assert!(root.join(&path).is_file(), "{}: {path} is not in the repository", e.code);
        }
        assert_eq!(source_url("facet-nope-99"), "https://gunnar.rs/nordisk/facett", "an unknown code links the repository");
    }

    /// ★ RA2-14: the link API was renamed (OWNER-DEC 3: the old repository constant, url
    /// fn and JSON field gave way to `SOURCE_REPO`, `source_url` and `source_url`) after
    /// crates.io's facett-core 0.2.0, which carries the old names. A rename is a break, so
    /// this crate is on a new breaking line above 0.2.
    #[test]
    fn the_renamed_link_api_is_on_a_breaking_line_above_the_published_0_2() {
        let v: Vec<u64> = env!("CARGO_PKG_VERSION").split('.').map(|n| n.parse().unwrap()).collect();
        assert!((v[0], v[1]) > (0, 2), "facett-core {} still claims 0.2's API", env!("CARGO_PKG_VERSION"));
    }

    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 source URL resolves to the component's source file.
    #[test]
    fn source_urls_resolve() {
        let e = lookup("facet-map-1").expect("facet-map-1 registered");
        assert_eq!(
            e.source_url(),
            "https://gunnar.rs/nordisk/facett/blob/main/facett-map/src/lib.rs"
        );
        assert_eq!(source_url("facet-map-1"), e.source_url());
        // An unknown code falls back to the repo root, never panics.
        assert_eq!(source_url("facet-nope-99"), SOURCE_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["source_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");
        }
    }
}