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

nordisk/facett

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

facett error codes — Phase 3: MOUNT identity + developer mode

Status: design, not implemented. Anchor (Rickard, 2026-07-31): "go through consumers and make sure they also create unique errors for their respective application, so robot UI — for example korp — can find out that if the same facet UI is used in 2 places in korp, it has individual error report (but ok, same base UI component)." Plus: "we want a developer mode, where a pink text appears with the facet-error name, or id, of that component."

Phases 1 and 2 are merged (facett-error-codes-design.md, facett-error-reactions-phase2-design.md). This phase closes the two gaps those left.


0. Where Phase 1+2 actually landed — measured 2026-07-31, not assumed

REGISTRY 13 components, 38 codes (facett-core/src/errcode.rs:182-278)
consumer facets carrying any code 1 of ~51 — only facett-korp
real raise sites in the whole workspace 12, and every one is the EmptyState half
registered "could not read" twins never raised 9 — map-4, map3d-4, graph3d-4, systemmap-3, syschart-3, korp-3, korp-4, korp-5, korp-6
facets overriding Facet::severity() 10 of ~80 — everything else structurally reports "I am showing real data" while blank
what the raise-site guard scans facet_err!( — 2 sites, both in facett-demo. The 12 real .error_code(" sites are invisible to it.

The catalog got its twins; the panes are still blind. That asymmetry is the whole reason ⚒ Build Thing read green four times while empty.


1. The gap Phase 3 closes — identity is per-COMPONENT, not per-MOUNT

facet-<component>-<n> answers which component failed. It does not answer which instance of it. When korp mounts the same graph pane twice, both raise the identical code, and:

The concept already exists on the test side. Phase 2's matrix is defined in (code × mount) rows — "all 30 (code × mount) rows have a production consumer call site". The matrix has always assumed a mount identity that the runtime never carried. Phase 3 supplies it.

The shape

A MountId assigned by the HOST at mount time, never by the component:

korp.graph.left        korp.graph.right
korp.map.main          dwarves.buildthing.deck

Consumer-side codes are a separate axis, and both are needed

Rickard also asked that consumers "create unique errors for their respective application." That is not the same as MountId:

axis owner answers example
facet-<component>-<n> facett which UI part failed facet-graphview-1
MountId host which instance of it korp.graph.left
<app>-<sub>-<n> consumer app what it means to THIS app korp-graph-7

korp already has the third axis (korp::diag::DiagCode, gRPC-mapped, in $KORP_STATE.diagnostics), and Phase 2 explicitly joined it to the facett axis. What is missing is the middle column. A pane that fails should be able to say, in one typed payload: "facet-graphview-1 at korp.graph.left, which korp classifies as korp-graph-7 and reacts to with Reaction::Diagnose."

Non-negotiable properties

  1. No string parsing anywhere. The original anchor stands: "not only the string tells it's wrong." If a robot test has to contains() its way to a mount, this phase failed.
  2. A mount id must be stable across runs or every pinned test rots.
  3. Absent mount id is an error, not a default. A facet mounted without one should be loud in dev mode, not silently "" — silent defaults are how the 9 unraised twins happened.
  4. severity() must be part of the same rollout. A mount identity on a pane that always reports Severity::Info while blank buys nothing.

2. Developer mode — make the identity visible

The ask: a toggle that paints, on each facet, a pink label carrying that component's error name / id.

This is the observability half of the same feature. C makes the identity exist; D makes it visible so a human can see what a test is about to pin.

Design


3. Order of work

  1. Widen the raise-site guard beyond facet_err!( to .error_code(". It currently guards 2 demo sites while 12 real ones go unchecked. Cheapest, and it makes every later step measurable.
  2. Raise the 9 registered-but-never-raised "could not read" twins. The catalog already has them. Start with korp-3 in GraphPane, which has no error slot at all — GraphModel cannot represent a failure, so a failed Cypher query renders identically to a query that returned nothing.
  3. Fix facet-korp-1's meaning. It is registered as "korp backend connection not established → Reaction::Reconnect, but its only raise site fires when the user pressed Run with no collector row selected, and severity() goes RED for it. A consumer following the table will attempt to reconnect because someone clicked a button without selecting a row.
  4. SearchPane cries wolf — severity() == Error whenever results.is_empty(), including a pristine never-queried pane whose own label says "type a query and Search". A permanently-red tab desensitises the gate; that is the other half of why a genuinely empty pane went unnoticed.
  5. Then MountId — plumb it, make absence loud, and land it with severity().
  6. Then developer mode, which is the cheapest way to verify 1–5 actually took.

4. The model to copy

facett-deploykey — cited by facett-buildthing's own doc comment as its model — carries a four-value state_label ("NO KEY FOUND", "KEY PRESENT — NEVER USED", "MINTED — NOT REGISTERED", …) and renders every path it looked at, in order, with a red "NO rung answered — every path below was empty".

That is the target shape: an enumerated evidence trail, not a claim. Compare the three current empty states, each of which asserts why it is empty while having no error path that could ever have told it otherwise — "this is an ABSENT declaration, not an empty build space", "this variant is not declared by the build thing", "no build yet".