Expand description
The APP SHAPE — App / Interface / Infrastructure, the one runtime shape every
workspace_nordisk suite app wears.
Designed 2026-08-02 with Rickard; the full argument is in
workspace_nordisk/.nornir/app-shape-design-2026-08-02.md, and its build-time sibling
(four atoms per suite member) is .nornir/SUITE-MODEL.md.
§The bug this exists to make impossible
An app stores a CHOICE and then never lets that choice decide what is drawn under it. The selection is written to a field and read back only where it is displayed — nothing downstream comes FROM it. So a pane offers controls that do not apply to what is selected, and hides ones that do.
Rickard, having hit it in two unrelated panes of one binary on one day: “the korp map
2d/3d choice should change settings below … do we see a pattern? infrastructure should
show correct choices/ui”. Both instances were real, and the second was measured: korp’s
chosen_infra had exactly ONE behavioural use in the entire UI — a filter hiding two
tabs — while 37 of 56 production draw_* fns named a backend directly. Choosing an
embedded backend hid two tabs and changed nothing else.
A control that is offered is a promise that it applies. 3-D lighting settings under
a 2-D map is not a missing feature; it is the UI asserting something false about its own
state. Same failure as two enabled flags that disagreed in one Settings pane, and as a
state file reporting a selected region while the select box rendered empty: two pieces
of code work out the same answer separately, somebody has to remember to change both,
and eventually nobody does. The cure is not a philosophy — delete the second copy.
§The shape
trait App { fn infrastructure(&self) -> &dyn Infrastructure; fn interface(&mut self) -> &mut dyn Interface; }Two halves, and that is the whole top level. Interface is recursive: a tab and a view
mode are the same kind of node at different depths, so the controls under a choice ARE
Interface::selected rather than being looked up. [Infra] hands out faces; a face
returning None is why a tab does not exist, which is what replaces a
is_spark_backend()-style predicate with a capability answering for itself.
§Why Interface is a supertrait of Facet, not a new pane contract
Facet already carries title / ui / state_json / update_json / severity /
component. Redeclaring a render here would be a second way to draw a pane — the
twin this whole module exists to prevent (LAW 5). So Interface: Facet adds exactly three
methods and inherits the rest.
All three are defaulted, so impl Interface for MyPane {} is a legal one-liner meaning
“a leaf: I have no sub-choices”. Every addition to Facet this year has been defaulted
for the same reason and that precedent holds here.
There is deliberately no blanket impl<T: Facet> Interface for T: it would make every
facet a leaf and forbid any pane from overriding, which is precisely the interesting
case. Opting in is one line and it is explicit.
Structs§
- Control
- One control, as DATA — never a widget.
Enums§
- Control
Kind - What kind of thing a
Controlis — enough for any renderer to draw it, and nothing about how.
Traits§
- App
- An app: a ui tree and a swappable infra. The whole top level.
- Face
- One backend capability. A face exists only if swapping [
Infra] swaps the implementation. - Infrastructure
- The swappable half of an app — hands out
Faces, and is the only thing that knows which backend answered. - Interface
- A node in the app’s ui tree — recursive, so a tab and a view mode are the same kind of thing at different depths.
Functions§
- selected_
path - Walk to the deepest selected node — the leaf the user is actually looking at.
Type Aliases§
- Key
- A stable machine key for one option — a tab id, a view mode, a layer. Not a label:
this is what
selecttakes, whatstate_jsonreports, and what a robot driver addresses, so it must survive a rename of the human-facing title.