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).codesays what broke;mountsays where. Neither alone is sufficient. MountIdis a host-owned string with a stable dotted form<app>.<surface>.<slot>, sokorp.graph.leftis 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
- 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. - A mount id must be stable across runs or every pinned test rots.
- 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. severity()must be part of the same rollout. A mount identity on a pane that always reportsSeverity::Infowhile 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 itsMountId. Pink (look::accent, not a hardcoded literal —facett-ganttis the cautionary case, it bypassesThemewith 8 hardcodedColor32::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. Noteassert!(r.drew())cannot catch this: a glyph is four vertices at 6 pt and at 60 pt, anddrew()is blind to colour entirely.
3. Order of work
- 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. - Raise the 9 registered-but-never-raised "could not read" twins. The catalog already has them. Start with
korp-3inGraphPane, which has no error slot at all —GraphModelcannot represent a failure, so a failed Cypher query renders identically to a query that returned nothing. - 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, andseverity()goes RED for it. A consumer following the table will attempt to reconnect because someone clicked a button without selecting a row. SearchPanecries wolf —severity() == Errorwheneverresults.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.- Then
MountId— plumb it, make absence loud, and land it withseverity(). - 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".