Skip to content
UPG is in early alpha. The specification is still evolving and may change between versions.

Modelling the Interface

A screen names a route. A surface names a place.

Most screens hold several places at once: a shell, a tool, a handful of panes, and the slots and action bars inside them. Each is somewhere features compete to sit, with a capacity, a rule for who wins, and often a different answer depending on how the product is configured. UPG records the place, the competition, and the configuration that selects between them.

9
surface kinds, from shell to ambient
13
edges into and out of a surface
1
graph holding every configuration
01The Place

The question no other entity answers

A screenscreenA distinct screen or view in the product answers which route is this?. A feature areafeature_areaA grouping of related features answers who owns this?. A bounded contextbounded_contextA DDD bounded context defining a service boundary answers what architecture is this?. None of them answers the question a designer and an engineer argue about most often: what else occupies the same place, and who wins when two things want it.

A surfacesurfaceA place inside a screen, the features that occupy it, and the rule that decides who wins it is that place. In Lantern, a fictional team documentation app, the page shell holds a header row, an editor canvas, a related-items region, and an inspector pane. Each is a surface. A feature claims one on a recorded edge, feature_occupies_surface, so the guest list of a panel is a query rather than an archaeology exercise through old tickets.

Contention over a place is settled constantly, and when the settlement lives only in prose it is rediscovered forever. Recording the place as an entity makes the guest list queryable and the arbitration durable.

Lantern, one screen
Page shellshell
Header rowregioncapacity: 4
Editor canvastool
Related itemsregioncapacity: 6additive
Inspector panepanecapacity: 1
containssurface_contains_surface

A screen names the route. The places inside it nest through surface_contains_surface, and each one carries its own capacity and composition mode. The related-items region is the one that changes with configuration.

02What A Surface Records

The properties exist because a field audit found each of them missing. Every one answers a question that was otherwise settled in a meeting nobody minuted.

How many fit
capacity counts the occupants one instance holds at once. Absent means unbounded. Zero is a real cap, a reserved place nothing may occupy, which is a different statement from having no cap at all. Capacity is always the intent the design asserts, so a region declared to hold one keeps saying one even after someone finds it rendering four; the gap is recorded as a debt item rather than by quietly editing the number up to match the bug.
How many of it there are
capacity counts occupants inside one instance; cardinality counts the instances, in UML multiplicity. instance_scope says what an instance belongs to: one shared across the product, or one per containing surface. Together they separate "Lantern has an inspector pane" from "every document pane has its own inspector, and three can show different content at once".
How the occupants relate
composition_mode distinguishes three shapes that need different governance. exclusive means one occupant wins and the rest are not rendered. additive means they coexist, so the open question is order rather than victory. chained means each occupant wraps the next, which is a trust model rather than a competition: an occupant that never calls the next one silently deletes everything downstream of it.
Whether anyone decided
arbitration_rule holds the answer, and arbitration_state says what kind of answer it is: enforced and written down, enforced but never transcribed, nobody decided, or safe only by coincidence. That last one is the dangerous state, because it looks settled from outside and breaks the first time an occupant is added.
03Partitioned Is Not Contended

A check that reads what the graph already knows

Contention is about displacement: who is not rendered. A surface holding no more than its stated capacityHow many occupants ONE INSTANCE of the surface holds at once, as a non-negative count. Absent means unbounded, `0` means a reserved place nothing may occupy. is partitioned rather than contended, because everyone fits and nothing is displaced. The contended-surface-without-arbitration check reads capacity and flags a surface only when its occupants exceed it.

An absent capacity is not an exemption. A surface that states no limit has stated no answer either, so several occupants is precisely the unrecorded decision worth naming.

The rule was calibrated against an audit of thirty surfaces in one product, later widened to forty-three. The check flagged ten of them: seven correctly, including a capacity-one panel with three registered occupants where opening one silently evicted whatever was open, and three wrongly, every one of them a surface whose occupancy sat at or below a stated capacity. Reading capacity removes those three and keeps all seven. That distinction matters more than the count suggests, because a check that fires on surfaces with nothing wrong teaches its user to write a placeholder rule to silence it, which is the one thing the schema tells them not to do.

Ordering is a different question from displacement. An additive region where every occupant fits still has a real question about what order they appear in, and capacity says nothing about it. That question stays documented rather than detected, because a second check with no field evidence behind it is how a family of checks becomes noise.

occupancy against capacity
Header rowcapacity: 4
Four occupants, room for four, positioned side by side. Everyone fits, so nothing is displaced and there is nothing to arbitrate.
partitioned
Inspector panecapacity: 1
Three features registered for one place. Opening one silently evicts whatever was open, and no rule records which wins.
contended
Banner stackcapacity: absent
No stated cap, so no stated answer. An unbounded place holding seven things has an unrecorded decision in it.
contended

The check compares occupants against the surface’s own capacity rather than counting edges. A place holding no more than it declared is partitioned, not contended. An absent capacity is read as one, because a surface that states no limit has stated no answer either.

04One Tree, Or A Family

The surface tree is not one tree

Lantern ships a lever that changes how a page shows its related items. Under one setting the related-items region holds a row of named chips, one per linked page. Under the other, a single summary badge takes their place and opens a panel when invoked. The badge is not the chips relocated: it holds one occupant where the chips hold six, it arbitrates differently, and the product ships a dismissible hint asking where the buttons went.

Reading the code shows one setting at a time, which makes the other easy to miss. The difference surfaces when the model is checked against what actually renders, and at that point three modelling instincts each fail in a different way.

Deleting the containment edge that looks wrong trades one error for another, because it is true under the other setting. Qualifying the edge (this region contains the chips when the flag is off) handles a relationship that moves but cannot express a node that does not exist: the chips and the badge are different surfaces with different capacities and occupants, and which one exists is the thing that changes. Duplicating the whole subtree is expressible, and wrong in practice: nothing marks the copies as alternatives, so a reader concludes the product renders both at once, and the contention checks read the doubled occupancy as real.

the stored graph
Related-items displayconfiguration_axis
named_chipssummary_badge
Related item chips
present_under: named_chips
surfacesurfaceA place inside a screen, the features that occupy it, and the rule that decides who wins it
Summary badge
present_under: summary_badge
surfacesurfaceA place inside a screen, the features that occupy it, and the rule that decides who wins it
alternates withsurface_alternates_with_surface
projection
one configuration
named_chips
Related item chips
Summary badge
the other
summary_badge
Summary badge
Related item chips

One graph holds both. A projection drops the surfaces absent under the chosen value, the composition edges qualified to other values, and the edges those leave dangling. Facts that name no axis survive every projection, which is why a graph declaring no variance reads exactly as it always did.

05If The Product Has One Configuration

A graph that declares no variance describes a product with one configuration, which is what every graph written before this existed already meant. Nothing below is required to model a product that ships one interface.

Name what is being described
Where a graph describes one configuration of a product that has several, the product records described_configuration: a plain-language label such as "standard plan, related-items chips". The qualification is then written down rather than remembered, and every fact in the graph inherits a condition a reader can see.
Keep it consistent
Model that configuration as ordinary invariant facts. Hedging individual edges in prose is worse than not hedging them: the graph is already scoped by the name recorded on the product.
It upgrades without migrating
A graph written this way is already a valid union, the union of a one-member family. Declaring an axis later turns the configuration that was named into the projection it was implicitly describing, and everything left unqualified stays true in every configuration, which is what it already was.
06The Union And The Projection

Where a product genuinely ships more than one interface, the stored graph is the union of the configuration family and a single configuration is a projection of it. Facts that carry no qualification belong to every member.

Name the lever, not the flags
A configuration_axis names one semantic lever and the closed set of values it can take. Lantern has one: related-items display, with the values named_chips and summary_badge. Two code flags that cannot be set independently are one axis with two values, not two axes, because modelling them separately asserts four configurations when only two exist. Each driving flag links to the axis with feature_flag_drives_configuration_axis, which keeps the mechanism in engineering and the lever in the product model.
Say what does not exist
A surface absent under some values carries surface_varies_by_configuration_axis with present_under naming the values it does exist under. This is the half an edge qualifier cannot express, and it is the common case when one design replaces another rather than moving it. In Lantern the chips row is present under named_chips and the summary badge under summary_badge.
Say what moves
A relationship that changes while both ends persist carries active_when on surface_contains_surface or feature_occupies_surface. Reach for it when an occupant moves to a different row under the lever, not when the row itself disappears. The qualifier is legal on those two edges and nowhere else, which is enforced rather than merely documented.
Say which are alternatives
surface_alternates_with_surface records that two surfaces are alternatives rather than neighbours. "One of these, depending" and "both, together" are different claims, and the variance declarations alone do not distinguish them: two unrelated surfaces gated by the same lever are also disjoint without being alternatives to each other. The edge is declared, and the graph is checked against it.
07Reading One Configuration

A projection is a way of looking, never a write. The union stays on disk; the tools select a member of the family from it.

Ask for one
query, get_tree and validate_graph take a configuration argument mapping an axis to one of its values. Surfaces absent under that value drop out, composition edges qualified to other values drop out, and edges left dangling go with them. An unknown axis or value is an error rather than a silent no-op, because a typo that quietly returned the union would leave a reader believing they see one interface while looking at all of them at once.
Ask what differs
Two queries that differ only in their configuration, the second passing the first result id as diff_from, return what changed between them. The answer includes relationships, not only nodes, which is what makes an occupant moving between rows visible: every node involved exists in both configurations, and the entire difference lives in the edges.
Validation projects by itself
In the union, occupancy edges qualified to different values are all present at once, so a slot holding one occupant per configuration reads as holding two. Anti-patterns are therefore evaluated per configuration: a finding true everywhere reports plainly, a finding true in only some reports annotated with where it holds, and a finding true only of the superposed union is suppressed and counted, because it describes a graph no configuration renders.
Contradictions are reported
The configuration_drift scope checks the declarations themselves: a value named that the axis does not declare, a qualifier on an edge it is not legal on, a declared alternation whose surfaces are both present under the same value. It also warns when a surface survives a projection with every containment parent absent, which is the most likely mistake when adopting the qualifier: qualifying one parent edge without providing the other branch.
08Where This Connects

The place, the lever, and the design layer they sit inside.