Skip to content

sparkles:ui architectural principles — Requirements (PRN)

Status: binding · Date: 2026-08-05 · Scope: the architectural rules sparkles:ui and its consumers are held to, each traced to its source in the Sean Parent research catalog.

These are not style preferences. They are the rules that make a single widget tree renderable to three backends without the per-backend divergence the toolkit exists to remove — and each one names a concrete failure the codebase has actually exhibited.

Design & rationale

The catalog's central claim is that complexity is anything that prevents local reasoning, and that the dominant source of it is the incidental data structure — "a data structure where there is no object representing the structure as a whole". A UI is unusually prone to this: view state accretes as loose locals, hierarchy hides inside element types, and every backend grows its own copy of a concept.

A second claim is specific to interfaces: the UI must not lie. A button that looks enabled must work; a state shown must be true. The mechanism is one state object queried many times, never several independent predicates that can disagree — which is exactly what happens when three backends each model selection their own way.

Ownership & structure (PRN1PRN4)

IDRequirementStatusTraces to
PRN1Every collection of related parts must have a Whole object that defines the structure's invariants and provides its interface. For a mutable value-like Whole, copies must be logically disjoint; immutable sharing, explicit borrowing, or exclusive ownership are permitted only when that policy is visible in the type. A struct merely containing mutable slices is not by itself an owner. Loose peer variables, or several arrays related only by convention, are not a data structure.partialdata-structures
PRN2Relationships must be represented in preference order Value > Identity (index/handle) > Reference (pointer) > Container. Where an index is used, the container that resolves it must be identifiable from the type, and each index must carry exactly one meaning.partialrelationships
PRN3Relationships must be explicit — modelled as data the code can run algorithms over — rather than implicit in pointers or recursion buried inside element types. Hierarchy is a flat arena with index links, not nodes containing nodes.fullwidget.d WidgetTree
PRN4Struct-of-arrays is permitted and often preferred, provided one Whole owns or explicitly borrows the arrays together, declares its copy/alias policy under PRN1, and holds their correspondence invariant (equal lengths, index alignment) as a checked invariant. Parallel arrays with no owner are forbidden.partialPRN1; contracts

NOTE

PRN2 and PRN4 are narrower than they may look, and deliberately so. The catalog itself writes size_t parent_index = npos and itself stores components in parallel arrays. Sentinel indices and struct-of-arrays are not the defect; the absence of an owner, and one encoding carrying several meanings, are.

Values & states (PRN5PRN7)

IDRequirementStatusTraces to
PRN5Illegal semantic states must be unrepresentable. A record whose fields are meaningful only for some value of a kind tag must be a sum type instead. The sum guarantees that only the active payload exists and makes handling exhaustive; it does not by itself make == total — every alternative must separately meet PRN6.partialhuman-interface; safety
PRN6UI objects that claim value semantics — props, state-machine values, presentation-free models and mutable Wholes — must be Regular: == is total and substitutive, and copies are independent. Borrowed immutable views must declare their equality and lifetime semantics. Handlers, delegates, resource handles and other non-comparable payloads stay outside the compared value and are addressed by identity or an explicit owner instead. A partially Regular type cannot be compared honestly.partialregular-types
PRN7Per-frame state transitions must be transformations — pure step(state, input) -> state functions — with a thin action layer that assigns the result. Other derived work may be a pure projection T -> U; painting, native input and I/O are explicit action boundaries. Mutating closures over shared local state are not an acceptable substitute for a transition.partiallocal-reasoning

Interface honesty (PRN8PRN9)

IDRequirementStatusTraces to
PRN8A semantic behavior must have one backend-independent definition wherever a target declares that capability. Backends may paint and measure differently, translate native input into the shared vocabulary, and report declared degradation; they must not duplicate the semantic state or transition. Independently-written semantic implementations of one concept are forbidden — correctness does not compose, and divergent behavior makes the interface lie about the same state.partiallocal-reasoning; human-interface
PRN9Semantic view state must be a presentation-free model queried by the view, never a second opinion cached in a backend. The same model serves every target. Device caches — glyph atlases, the prior terminal grid, native input edges — are permitted when they contain no semantic state and are derived or invalidated from explicit inputs. This property-model discipline is what makes "same model, different UIs" true.partialhuman-interface

Discipline (PRN10PRN12)

IDRequirementStatusTraces to
PRN10A loop is raw when it appears inside a function whose purpose is broader than the algorithm the loop implements. Such loops, and repeated logic generally, must be lifted into one named algorithm with stated requirements, guarantees and complexity. A per-frame inner loop may remain inside that named algorithm; it is not raw once the function's whole purpose and contract are the loop's operation.partialalgorithms; cpp-seasoning
PRN11Internal programmer obligations must be expressed as narrow contracts in in/out/invariant blocks — preconditions, postconditions and invariants, compiled out in release. Invalid user, terminal, network or file input is runtime validation and must not be hidden in a release-elided contract. The lifted pure algorithms of PRN10 must be covered by property-based tests in addition to focused examples.partialcontracts
PRN12The widget representation is a finite, closed sum type over a flat arena, not a class hierarchy or heap-allocating type erasure. This is a Sparkles decision: the closed vocabulary makes backend handling exhaustive and preserves the steady-state no-allocation path. The sum supplies PRN5's active-payload and exhaustiveness guarantees; total equality still requires every alternative to satisfy PRN6.partialvalue-semantics; WGT3; NFR2

NOTE

PRN12 is a project decision made with the catalog's trade-offs in view, not a rejection of type erasure in general. Parent's value-semantic erasure is the right shape for open, small interfaces and can use a small-buffer optimization. The widget payload is different: its vocabulary is deliberately closed, every backend must handle every alternative, and NFR2 requires a steady-state no-allocation path. Those local constraints select the sum.

Known failures and open gaps

These are the concrete defects the requirements above were written against. Open gaps have canonical issue entries; resolved cases remain here as regression rationale rather than as untracked work.

ViolationRule breachedTracking
A frame loop holding peer state groups and mutating closures, with no GUI-state WholePRN1, PRN7HUE-O1
A value-like Whole containing mutable slices whose default copies aliasPRN1, PRN6UI-O1
Records reaching into arrays owned by other objects via sentinel indices with three meaningsPRN2historical migration failure
A tagged record whose own documentation says only the kind-named fields carry meaningPRN5, PRN12UI-O2
Parallel arrays declared in several places with no owner and no length invariantPRN4historical migration failure
One semantic behavior implemented separately per backend, with divergent resultsPRN8resolved by MIG12; retained as regression case
One interaction (selection) modelled three incompatible waysPRN8, PRN9resolved by STM3; retained as regression case
Transient state as bare counters advanced by hand at each call sitePRN5, PRN7resolved by STM6; retained as regression case

Module coverage

These requirements are cross-cutting: they bind every module in libs/ui, libs/input, the backend adapters, and any consumer building a widget tree. Per-module tracing lives in the sibling specs; this page is the rule set they share.

Relationship to existing specs

PieceRole
Sean Parent catalogthe evidence base these rules are drawn from
widgets.md WGT/VMDwhere PRN5, PRN6, PRN9 and PRN12 are discharged
state-machines.md STMwhere PRN7 and PRN9 are discharged
backends.md TGTwhere PRN8 is enforced — shared semantics, adapted targets
Open issuesimplementation gaps deliberately deferred by this docs pass
Code stylethe D-level conventions these rules sit above

Overview · Layout · Widgets · Migration