ADR-030
ID:ADR-030Status:proposed

Arrangements name topologies; variants are modifiers; semantics are rune-locked

Source:SPEC-080
claude/nifty-ptolemy-80epot View source
Implemented by 1
Informs 1
Related 22
Branches 3
History 10
  1. 92b7c77
    Content editedby bjornolofandersson
  2. 07900ea
    Content editedby bjornolofandersson
  3. 4c42463
    Content editedby bjornolofandersson
  4. 2fe501b
    Created (proposed)by bjornolofandersson
  5. db0a158
    Content editedby Claude
    docs(plan): renumber the sequence-duplication bug to BUG-024
  6. ee315d6
    Content editedby Claude
    docs(plan): WORK-585 — name the Steps item container; normalise Playlist
  7. dd34796
    Content editedby Claude
    docs(plan): ADR-030 rule 6 — geometry comes from the arrangement, not th
  8. 76b06bc
    Content editedby Claude
    docs(plan): ADR-030 — split the promotion rule; add a provisional tier a
  9. 6f9a8d2
    Content editedby Claude
    test(transform): WORK-584 Q1 — layout does reach a child rune; ADR-029 l
  10. 4175521
    Content editedby Claude
    docs(plan): record the theme-complexity findings — SPEC-137, ADR-030, 2

Context

Three mechanisms decide how a rune's children are placed, and each can only be pointed at one narrow kind of content:

MechanismDecidesCoupled to
layout tree (SPEC-081)nesting and order of named slotsnothing — this part generalises
blocks[].layout (SPEC-080)geometry: 'bar' | 'definition-list'fields projected from metaFields
data-sequence (dimension)geometry of ordered items<ol> elements and their > li children

layout says where things nest. The other two say what shape a group takes, but neither can be applied to arbitrary content: a rune cannot declare "arrange my child runes as a grid" or "arrange this content region in columns". That is why bento, gallery, grid, palette and swatch each carry their own grid CSS — not because a grid primitive is inexpressible, but because there is nowhere to declare one.

ADR-018 governs the author-facing layout attribute — the values an author writes in markdown. Nothing governs the theme-facing assembly vocabulary. This decision is its sibling.

The question that forced it

If the vocabulary grows, at what granularity does it cut? Concretely: if "connected dots" (today's sequence: 'connected', used by timeline and itinerary) became its own named arrangement, a theme could give a playlist the timeline treatment.

That must not be allowed, and the reason generalises. timeline.ts:44 emits new Tag('time', {}, [attrs.date]) — each timeline entry carries its own positional label. There is no "event 4"; the marker is a dot precisely because a numeral would be false information. On a playlist the number is the track number, an identifier a listener says out loud. Restyling a playlist as a timeline does not change how it looks. It deletes the track numbers.

So numbered vs connected is not an appearance cut. It encodes "is the ordinal itself information?" — a fact about the content, not about the theme.

Decision

Six rules governing how the arrangement vocabulary may grow. They govern a vocabulary that does not exist yet; that is deliberate, and rule 5 says why.

1. Topologies get names

An arrangement names a topology — the shape of the relationship between a container and its children. stack, row, grid, split, ladder, tree, rail, track are topologies. timeline, steps, playlist are not; they are runes that use one.

2. Variants are modifiers, not names

Direction, marker, connector, wrap and density are modifiers on a topology. Promoting a combination to a name produces a combinatorial explosion: naming timeline immediately owes timeline-horizontal, timeline-grouped, numbered-horizontal, numbered-grouped.

3. Semantic modifiers are rune-locked; cosmetic ones are theme-free

marker: ordinal | positional | none is rune-owned and immutable — it states whether the index is data. The marker's chrome (circle, square, bare numeral, colour, weight), the connector's rendering, spacing, and responsive direction collapse are entirely the theme's.

This is ADR-028's principle one level down: a theme may not redefine what a section is, and it may not redefine whether an index is information.

4. Arrangements nest

A container's topology and its items' internal layout are independent. A playlist is a ladder of rows; a steps list is a ladder of stacks. This is the capability whose absence currently forces bespoke CSS: after BUG-024 removes track's duplicated counter, the remaining display: flex / flex: 1 / ellipsis in skeleton/styles/runes/track.css is the track's inner arrangement, which no mechanism can declare.

5. Two bars, by what the addition is for

An earlier draft had one rule — three existing implementations before promotion — which is right for one kind of addition and circular for the other. Existing implementations only exist for shapes the current vocabulary permitted, so counting them approves consolidations of what people already hand-rolled and can never approve anything new. rail reads as having one consumer not because rails are rare, but because nothing could declare one: every would-be rail became bespoke CSS and does not register as an implementation of anything.

5a — a consolidating topology needs three existing implementations. It replaces bespoke CSS that already exists, so the claim being made is this pattern recurs, and the repetitions are the evidence. Two is a coincidence; three is a pattern. A generic primitive that is 80% right is worse than the bespoke CSS it replaced, because the remaining 20% is fought silently in every consumer and the cost is invisible until the fourth one. Ships stable.

5b — an enabling topology needs a contract, a case, and a tier. It offers what nothing does today, so there is nothing to count. Instead it needs:

  • a contract statable without naming any rune — if the definition needs an example to be intelligible, it is not a topology yet;
  • at least one concrete design that wants it, with a sketch of the markup and CSS it would produce;
  • no overlap with an existing topology (see rule 1 — a variant is not a topology);

and it ships provisional: recorded in the theme contract as unstable, excluded from the stability guarantee, and changeable without a migration.

Promotion from provisional to stable requires a second independent consumer that adopted it without needing its shape changed. That is what the old three-implementation bar was a crude proxy for. The thing a second consumer teaches is not "people want this" — it is "the contract survived contact with a use case its author did not have in mind."

6. Vocabulary is cheap; mechanism is expensive

Rules 5a and 5b govern mechanism. They do not govern names.

sections has a closed seven-value role vocabulary (header | preamble | title | description | body | footer | media), which is right because a role changes what the engine does. Presentational groups (rule 2 of ADR-029) have no vocabulary at all: every theme invents its own wrapper names. A conventional, open set of group names — byline, meta, actions, aside — is worth publishing at a much lower bar than a topology, because a wrong name costs nothing (add another) while a wrong mechanism costs migrations.

A group name carries no geometry. Its shape comes from the arrangement it declares, exactly as any other container's does — so a byline is a group named byline that arranges as a row, and skeleton styles the arrangement, never the name. This is what keeps the vocabulary free: nothing is stamped on a node for being called something, so a rune part that happens to share a conventional name inherits nothing and needs no reserved-name rule to protect it. Opt-in is by declaring an arrangement, not by choosing a label.

The capability is already there, under the pre-rename spelling. A layout entry's attrs are spread onto the wrapper the engine creates (placeNames, engine.ts), and skeleton/styles/dimensions/metadata.css already styles [data-zone-layout="bar"] generically — display: flex, flex-wrap: wrap, align-items: center, plus data-wrap and data-align modifiers. So this works today, in any theme, with no new code:

byline: {
  tag: 'div',
  attrs: { 'data-zone-layout': 'bar' },
  children: ['track-artist', 'track-duration'],
}

Pinned by case B2 in packages/transform/test/child-rune-layout.test.ts. What the rename buys is type safety rather than capability: stamping the attribute through attrs is a stringly-typed back door that nothing validates, where a first-class arrange key on LayoutEntry would be checked against the vocabulary.

Rule 6 also closes a concrete gap. A plugin shipping CSS for its own runes cannot target a group a theme invented. Without a conventional vocabulary, plugin CSS and theme structure cannot meet.

Rationale

Rule 5 is the load-bearing one, and splitting it changes the answer materially:

CandidateExisting consumersBarVerdict
row, pairsshipped as bar / definition-list—rename only
ladderhowto, recipe, steps, track, timeline, itinerary5aconsolidation — it already exists as data-sequence
gridbento, gallery, grid, palette, swatch5astable, and genuinely new capability
splitcompare, comparison5a2 — wait for a third
railprogress5bprovisional — contract is statable, tolerance is the case
track—5bprovisional — mix is the case
radial——not a topology; a ladder direction (rule 2)

Under the single old bar, everything below grid was blocked and the honest summary was "grid is the only arrangement that adds capability." That result was an artefact of the rule, not of the domain: it measured what the existing vocabulary had permitted people to build by hand.

Under 5a/5b the vocabulary can grow where a contract is genuinely clear, without freezing guesses into a public surface — the provisional tier absorbs the uncertainty that the old rule handled by refusing outright.

Rules 5a and 5b fail in opposite directions, which is why both are needed. 5a's failure mode is the near-miss: rail looked like it had two consumers — progress and budget — until budget was read. skeleton/styles/runes/budget.css is entirely justify-content: space-between rows with no proportional bars anywhere; it is a stack of rows built from the two primitives that already ship. Counting plausible-sounding consumers rather than reading code would have shipped rail as stable on false evidence. 5b's failure mode is the opposite — a well-argued contract with no real use — which is why it ships provisional and why promotion requires a consumer the author did not anticipate.

Consequences

The first shipment is a rename plus one stable and two provisional additions. data-zone-layout and data-sequence become values of one data-arrange vocabulary; grid lands stable; rail and track land provisional. Theme authors learn one vocabulary with uniform applicability instead of three with different coupling rules.

The provisional tier needs teeth or it is a comment nobody reads. If nothing marks a provisional arrangement as unstable, it becomes stable by adoption and the tier has achieved nothing. Minimally: the theme contract records each arrangement's tier, and a theme using a provisional one is told so at build time. Without that, 5b is strictly worse than the old rule, because it ships guesses with the appearance of a guarantee.

Provisional is not a parking space. An arrangement that sits provisional across several releases with no second consumer is evidence the contract was wrong or the need imagined, and should be removed rather than left to accrete users. Removal is the tier's whole point and has to actually happen.

radial is not a topology. Under rules 1 and 2 a cycle is a ladder whose ends join: stack mode is a vertical ladder, strip is horizontal + wrap, wheel is radial + wrap, grid is a grid. radial is a direction value alongside vertical and horizontal, and wrap is the one genuinely new modifier — it is what distinguishes a cycle from a list.

The rename needs addressable item containers. sequence: is declared rune-level and applied by walking the tree for <ol> elements; arrange is declared per container, which requires that container to be nameable in the layout tree. Five of the six sequence consumers already name theirs through refs — HowTo, Recipe, Timeline, Itinerary — or directly on the tag (Playlist). Steps does not, and WORK-585 closes that gap independently of this decision.

Nesting needs no new machinery. Rule 4 is what makes a theme able to change a playlist from rows to cards. WORK-584 Q1 established that the engine already supports it: every data-rune node gets its own assembly pass from its own config, so a container's topology and its items' inner layout are declared independently as a matter of course. An arrangement vocabulary inherits this rather than having to build it.

Semantic locking needs somewhere to live. Rule 3 requires the engine to distinguish rune-owned from theme-owned modifiers on the same arrangement. ADR-029 draws the equivalent line for containers — a semantic container carries data-section, a presentational group does not, and sections is identity-guarded so a theme cannot cross it. Modifiers need the same treatment, and the two should use one mechanism.

A group-name vocabulary is separable and costs almost nothing. Rule 6 is a documentation page and an agreed list — no engine change and no CSS. Themes can already create named groups through layout (WORK-584 Q1) and already give them shared geometry by declaring a zone layout, which skeleton already styles. It can ship before any arrangement work and is the one part of this decision with an immediate audience, plugin authors included: they have no group name they can rely on today when shipping CSS for their own runes.

Composition changes what rule 5b's "a concrete design that wants it" means. Recorded after the fact, because the justification moved. When this decision was written, an arrangement vocabulary consolidated bespoke CSS — five runes with their own grid, one with its own row — and the gain was tidiness plus a theme's ability to restyle.

SPEC-145 makes it enabling. A composed rune has no block (D2), so it has no selectors of its own, and its appearance is whatever the theme gives the primitives it places. For a first-party composition a theme can reach it by name through contextModifiers (SPEC-145 D16). For a user-authored one it cannot: Lumina will never have heard of wine-tasting-note, and ADR-037 makes user-authored compositions the only way a rune reaches a site from outside this repo. So a vocabulary the theme styles without naming the rune is the only route those runes have to being styled at all.

Rule 4 is the specific case, and it now has a measured consumer. SPEC-155 found playlist composable on every axis but appearance, and the appearance gap is exactly the ladder-of-rows this decision named: 98 lines in lumina/styles/runes/track.css, every selector keyed on .rf-track / .rf-track__*, all of it unreachable once the rune has no block. That satisfies 5b's "at least one concrete design that wants it" with a design that exists rather than a sketch — and 5a's three-implementation bar is separately met for the row/ladder case by track rows, cast's list roster and steps.

This decision still does not authorise building anything. It constrains a later spec. If that spec never comes, nothing is owed — but the thing owed if composition ships without it is now stated: composed runes that only the theme's author can style.

Rules 5a and 5b have been borrowed for a second vocabulary, and that extension should be explicit rather than assumed. Every rule here is written about topologies — rule 1 defines an arrangement as the shape of a container-to-children relationship, and 5a/5b say "a consolidating topology" and "an enabling topology". SPEC-159 applies the same two bars to chrome axes (materiality, variation), which are qualities of a single surface and not relationships at all, and its D8 records walking into 5a's counting trap on the way.

The borrowing is sound and the generalisation is worth stating: 5a and 5b are bars on a theme-facing vocabulary term, whatever the term describes. What transfers is the asymmetry — a term that replaces existing hand-rolled CSS is claiming recurrence and owes instances; a term that enables something nothing can express today has nothing to count and owes a contract, a case and the provisional tier. Nothing in that reasoning depends on the term being a topology. Rule 3's semantic/cosmetic split transfers too, and lands cleanly: chrome is cosmetic by construction, so chrome axes are theme-free and none of them may encode whether something is information.

Which makes the provisional tier's teeth overdue rather than pending. The consequence above — "the provisional tier needs teeth or it is a comment nobody reads" — was written with rail and track in view. SPEC-159 D9 adds two more provisional terms in a different vocabulary, so there are now four queued behind a mechanism that does not exist, and the first shipment of either vocabulary is the one that decides whether 5b is a tier or a disclaimer. Two provisional terms with no build-time signal are a rounding error; four across two vocabularies is a convention.

Rule 2 forbids naming a combination; SPEC-159 Part 3 names combinations deliberately. Both are right, and the reconciliation belongs here. Rule 2's target is the axis: promoting timeline to an arrangement value owes timeline-horizontal and timeline-grouped, because every modifier multiplies through the name. A composite intent sits in a separate layer above the axes, resolving to axis values rather than joining them — so it adds no value to any axis and nothing multiplies. That is rule 6's territory (vocabulary is cheap, mechanism expensive) and the governance follows rule 6's bar, not rule 5's.

The reading generalises, and saying so now costs nothing: a composite arrangement would be allowed on the same terms — a name resolving to ladder plus its modifiers is not a new topology, exactly as keepsake is not a new chrome axis. Nothing proposes one, and the rule that would govern it is already written.

The guard that keeps the layers honest is rule 6's own: "a group name carries no geometry." Its chrome analogue is that a composite intent carries no CSS — if a preset can paint, it has stopped being a name for axis values and become a specification, which is the mechanism SPEC-159's first revision proposed and its rewrite rejected.

Alternatives considered

Name arrangements after their appearance — timeline, steps, carousel. Reads well and matches how people talk. Rejected: it makes presentation and semantics indistinguishable, so a theme swapping numbered for timeline looks like a style change and is data loss. It also multiplies names per variant combination.

Let themes choose any arrangement freely, with no locked modifiers. Maximum theme expression. Rejected on the playlist case — the track number is content, and a vocabulary that lets a theme delete content is not a styling vocabulary.

Freeze at bar and definition-list; keep everything else bespoke. No new surface, no governance needed. Rejected: it accepts that every rune with novel geometry pays full CSS cost forever, and leaves the blocks/metaFields coupling — which is the thing actually blocking grid declarations — unaddressed.

Define the full vocabulary now and let consumers catch up. Faster to a coherent-looking system. Rejected by rule 5a, and by the rail/budget near-miss that produced it — a vocabulary assembled from plausible-sounding names rather than read code ships primitives on false evidence.

One bar for everything: three existing implementations, no exceptions. This was the first draft, and it is simpler to state and to enforce. Rejected as circular: existing implementations only exist for shapes the vocabulary already permitted, so the rule can approve consolidations and nothing else. It would also have blocked rail indefinitely on the grounds that nothing declares a rail today — which is true, and is the problem rather than the answer.

Ship enabling topologies stable, and accept the compatibility risk. Serves theme authors immediately with no tier machinery. Rejected because the cost of a wrong arrangement is not the code but the migration: once themes depend on a contract it cannot be changed quietly. The provisional tier gets the same availability at a fraction of the risk, and the only thing it asks for is honesty about which parts are settled.

References

  • SPEC-080 — the metaFields / blocks / layout assembly model this governs
  • SPEC-081 — declarative structure assembly
  • ADR-018 — canonical layout vocabulary; the author-facing sibling
  • ADR-028 — attribute applicability is rune identity; the source of the semantic-locking principle
  • ADR-029 — structural assembly is theme-owned; draws the container half of the same line
  • BUG-024 — the sequence duplication that exposed the <ol>/<li> coupling
  • SPEC-145 — composed runes; why this vocabulary became enabling rather than consolidating
  • SPEC-155 — the media audit; the measured consumer for rule 4
  • ADR-037 — users author composed runes only; the audience that cannot be styled by name
  • ADR-038 — a behavior binds on a data contract; the same rune-agnostic principle for state and binding
  • SPEC-156 — the later spec this decision constrains, scoped to ladder and row; both clear rule 5a, and it finds rule 3 currently unenforced
  • SPEC-158 — where "semantic locking needs somewhere to live" is answered, and where rule 3 becomes enforceable
  • SPEC-159 — chrome as intent; the second vocabulary to adopt 5a/5b, the second client of the provisional tier, and the composite-intent layer rule 2 had to be read against