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.
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.
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.
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.
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.
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.
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.
present_under: named_chipssurfaceA place inside a screen, the features that occupy it, and the rule that decides who wins itpresent_under: summary_badgesurfaceA place inside a screen, the features that occupy it, and the rule that decides who wins itOne 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.
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.
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.
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.
The place, the lever, and the design layer they sit inside.
The place itself: its kind, persistence, capacity, cardinality, composition mode, and the rule that arbitrates between its occupants.
The lever whose values select which surface tree the product renders, with its closed value set and the flags that drive it.
Journeys, concepts, prototypes, design systems, and the surfaces features claim inside them.
Every entity, edge, property, and check, including the projection semantics this page describes.