# 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:
- a robot-UI test cannot assert **which** pane broke;
- a consumer cannot react differently to the two mounts (retry one, diagnose the other);
- two failures that are genuinely different are, once again, indistinguishable — the
exact defect class this whole scheme exists to kill, one level up.
**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
```
- The component stays **one shared UI part** — no forking a facet to get a second
identity. Rickard's constraint: *"but ok, same base UI component."*
- The raised error becomes the pair **`(code, mount)`**. `code` says what broke;
`mount` says where. Neither alone is sufficient.
- `MountId` is a host-owned string with a **stable dotted form** `<app>.<surface>.<slot>`,
so `korp.graph.left` is greppable, sortable and diffable across runs.
- It rides in the **same dual channel** the code already uses — never a second mechanism.
### 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
- **Toggle:** env `FACETT_DEV_IDS=1`, a CLI flag, and a keyboard chord — all three, since
the demo, the robot harness and a live app each reach it differently.
- **What it paints:** `facet-<component>-<n>` when a code is live; otherwise the
component slug plus its `MountId`. Pink (`look::` accent, not a hardcoded literal —
`facett-gantt` is the cautionary case, it bypasses `Theme` with 8 hardcoded
`Color32::from_rgb`).
- **Where:** a corner badge, drawn last, never displacing layout — dev mode must not
change what it is describing.
- **★ It MUST also land in the accessibility tree, not only in pixels.** This is the
load-bearing detail. Robot tests should read the id as a **node**, never OCR a colour.
Today `facett-korp`'s two panes are the only ones putting codes in the a11y tree via
`.error_code("…")`; dev mode should extend that, not invent a parallel channel.
- **Verification:** assert the badge in the a11y tree with the harness, and assert its
ink with `RenderReport::ink_colors` — the pink must actually be emitted, not blended
away. Note `assert!(r.drew())` cannot catch this: a glyph is four vertices at 6 pt and
at 60 pt, and `drew()` is blind to colour entirely.
---
## 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"*.