Relationships
Related 29
Branches 5
Summary
ADR-030 governs how an arrangement vocabulary may grow and closes by saying "this decision does not authorise building anything. It constrains a later spec." This is that spec, scoped deliberately narrow: two topologies, ladder and row, because those are the two the evidence supports and the two SPEC-145's composed runes need first.
Revised on 2026-10-27 against the code after v0.42.0. The first draft found both topologies hand-written and missing generically. Re-read, both already exist generically: the row as [data-zone-layout="bar"], and the ladder as the sequence dimension ([data-sequence]). What is missing is aperture: a composition template cannot reach either. So the deliverable shrinks to one template tag, {% group %}, with two arrangement values. The two identity holes the first draft found were closed by SPEC-158 in v0.40.0.
Decision numbers are kept from the first draft, because other specs cite them. D2, D3 and D8 are revised in place; D4 and D5 are recorded as shipped; D10 moves to the ADR-030 review; D12–D14 are new.
The row exists, once generically
Generic, packages/skeleton/styles/dimensions/metadata.css:
[data-zone-layout="bar"] { display: flex; flex-wrap: wrap; align-items: center; } [data-zone-layout="bar"][data-wrap="false"] { flex-wrap: nowrap; } [data-zone-layout="bar"] [data-align="end"] { margin-left: auto; }
symbol, bar and api key on it. Rune-specific copies live in packages/skeleton/styles/runes/track.css and playlist.css: the same flex, the same marker box, the same flex: 1; min-width: 0 name cell, and margin-left: auto on the duration, which is the bar's right-push. A track row is a bar, confirmed in CSS rather than by analogy.
Today a bar is reachable from the bar rune, or from blocks[].layout over metaFields-projected fields. It is not reachable over ordinary named slots, which is what a composition template has.
The ladder exists too: it is the sequence dimension
The first draft said the ladder container "exists five times, and generically nowhere." That was wrong. The generic container is the sequence dimension:
- Values:
numbered,connectedandplain(RuneConfig.sequence,packages/transform/src/types.ts). - Structure,
packages/skeleton/styles/dimensions/sequence.css: the list reset, the counter origin, the item gutter and the marker box fornumbered; the item geometry and marker box forconnected; the reset alone forplain. Look,packages/lumina/styles/dimensions/sequence.css: marker fill, connector lines, type. - Stamping: the engine's
annotateSequence(packages/transform/src/engine.ts) setsdata-sequenceon every<ol>inside a rune whose config declaressequence. Seven runes do:howto,recipe,steps,playlist(numbered),timeline,itinerary(connected), andplotthrough a modifier.
What the per-rune CSS is. Seven skeleton files reset a list for their items: playlist, steps, timeline, itinerary, howto, recipe and cast. Four of them also hand-roll a counter that the dimension already provides, which BUG-024 records: the two land at different specificities, so which one wins differs per rune. That is duplication of a generic contract, not the absence of one.
cast is a weaker case than the first draft said. It was called the strongest single piece of evidence, a ladder no config knows about. Its container has three arrangements, grid, list and carousel (packages/skeleton/styles/runes/cast.css). Only list is a ladder.
So the one genuinely missing generic part is small: a ladder with no sequence, a list of peer items in a column with the marker reset and nothing else. plain is close, but it is stamped only through a rune's sequence config.
What a composition cannot do
A composed steps needs an <ol> of its steps, stamped data-sequence="numbered". It has no way to get one:
- No
sequence.composedRuneConfiggeneratesmodifiers,metaFields,blocksandprovides(SPEC-169, What the parser accepts). SPEC-169 D6 routessequenceto this spec. - No list around an
eachslot. A template writes Markdoc. It cannot wrap the items aneachslot produces in an<ol>with one<li>per item. - No row over slots. A template that wants bar geometry must place the
barrune, which carriesdata-rune="bar", a name-resolution boundary (SPEC-145 D19).
SPEC-169 D4's own steps example already writes the answer:
{% group name="steps" layout="ladder" %} {% slot name="items" each %} {% section %}{% slot /%}{% /section %} {% /slot %} {% /group %}
Variants of an item's arrangement
The worked case: display a track as a name with a byline underneath rather than as one row.
row form: [1] Speak to Me · Pink Floyd ————————→ 1:13 · Mar 1973 byline form: [1] Speak to Me Pink Floyd · 1973 · 1:13
The parts are identical. The item's topology goes row → column, and three sibling parts are grouped. That is rule 4 nesting, not a new topology, and byline is one of the group names rule 6 names (byline, meta, actions, aside).
On the declared path, blocks already does it: a BlockDef is a named group of meta-fields with a layout, and { field: 'duration', align: 'end' } emits data-align="end", which is the right-push track.css hand-writes. Measured on 2026-10-27: Track's config is still { block, parent, defaultElevation, editHints }, with no metaFields and no blocks. Its artist, duration and date are hand-built spans.
Selection follows who wants the change:
| Wants it | Mechanism | Status |
|---|---|---|
| The theme | variants over layout (SPEC-091) | ships |
| The composition author | SPEC-145 D15, several templates | decided, not built |
| The page author, per instance | a parent attribute reaching its items | D8 |
The grouping-device hazard
An engine-made group (a blocks zone, a created layout wrapper, or the {% group %} of D2) carries data-name and no data-rune. A group made by placing a rune, {% bar %}…{% /bar %}, carries data-rune="bar", which is a name-resolution boundary: findAllByName stops at it, so a schema property sourced from a node inside that group disappears with no diagnostic (SPEC-146 Problem 2). track survives by luck: every property in trackSchema is attribute-sourced. SPEC-145 D19 states the rule for templates; D9 here records it.
Decisions
D1 — scope is ladder and row, and both are consolidations
ADR-030 rule 5a, not 5b. Both topologies have generic implementations already and several hand-written copies. No other topology is in scope: grid and split are plausible consolidations with their own evidence and their own spec.
D2 — a template reaches both through {% group %}, an engine wrapper with an arrangement
Revised 2026-10-27. The first draft said the row needed aperture and the ladder needed a new contract. Both need the same thing: a way for a template to declare an arrangement on an engine-made group. That is ADR-030 rule 6's form, made a template tag:
{% group name="byline" layout="bar" %}…{% /group %} {% group name="steps" layout="ladder" %}…{% /group %}
- It is an engine wrapper. It renders with
data-nameand nodata-rune, so it is safe where the rune publishes schema (D9, SPEC-145 D19). A composed rune has no block (SPEC-145 D2), so a group gets no BEM class: themes see itsdata-nameanddata-zone-layout. nameis required, and a convention, not a type: it carries no geometry (D6).layouttakesbarorladder. Nothing else, until a topology clears ADR-030 rule 5a in its own spec. An unknown value is rejected at schema construction, naming it.barrenders a<div data-zone-layout="bar">over whatever the group holds, the existing row contract. A child that should push to the end is written in its own{% group name="…" align="end" %}, which rendersdata-align="end".ladderrenders a list (D3).- It is a template tag only. Authors do not write
{% group %}on a page; thebarrune remains the author-facing row (SPEC-145 D12's placement set is unchanged).
D3 — the ladder is the sequence dimension, plus a reset for the unsequenced case
Revised 2026-10-27. The contract, stated without naming a rune: a list whose direct children are peer items, each free to declare its own inner arrangement, with a shared marker gutter when the rune declares a sequence.
- Element.
<ol>when the definition declares asequence(D12),<ul>otherwise, withdata-nameanddata-zone-layout="ladder". - Items. Each iteration of an
eachslot placed directly in the group, or else each top-level node the group holds, is wrapped in an<li>. - Markers are the
sequencedimension's: the ladder carriesdata-sequencewith the definition's value. No second marker contract is written. - The unsequenced ladder gets the one missing rule:
[data-zone-layout="ladder"]resets the list and flows its items in a column, about six declarations in skeleton.cast'slistmode is its first native consumer. - "Optional separation between items" stays one hook, not modifiers. If it becomes three modifiers, the topology is really two and this decision is wrong.
D4 — sequence is identity: shipped
Closed by SPEC-158 D2 in v0.40.0: sequence is in IDENTITY_FIELDS (packages/transform/src/identity-fields.ts), and sequenceDirection stays presentational.
D5 — layout.attrs cannot write an identity attribute: shipped
Closed by SPEC-158 in v0.40.0: a layout attrs entry that writes an attribute an identity field derives is rejected (identity-fields.ts, cited there as SPEC-156 D5).
D6 — a group's geometry comes from its declared arrangement, never from its name
ADR-030 rule 6, made operative by D2: byline is a group named byline that arranges as a row. Nothing is stamped on a node for being called something, so no reserved-name rule is needed.
D7 — the byline variant needs no new mechanism, only adoption
metaFields, blocks, two layout values and a variants axis all ship. It is blocked only by Track not declaring its meta fields, which is still true on 2026-10-27.
D8 — the per-instance cascade is narrowed to author-written children
Revised 2026-10-27, as SPEC-171 D4 asked. Three cases were found: bento's media-position, playlist's default artist (track.artist ?? artistValue), and playlist's child kind.
SPEC-145 D32 now covers a parent passing a value to children the template places: the template writes it on them. What D32 cannot reach is children the author writes inside a slot: the tracks in a playlist, the cells in a bento. A template places the slot, not the runes inside it. All three cases are of that kind, so the childDefaults mechanism is still needed, scoped to author-written children, and remains a separate spec. SPEC-159 keeps the theme-owned half.
D9 — a composed template groups with an engine wrapper, not a placed rune
Where schema is involved; with no schema table, either device is safe. {% group %} (D2) is the engine wrapper a template writes. SPEC-145 D19 states the same rule.
D10 — carousel and rail: moved to the ADR-030 review
rail appears only in ADR-030's vocabulary table, marked provisional, and nowhere in code. carousel has a contract and three adopters (SPEC-100). ADR-030 also lists track as a topology name while track is a rune in @refrakt-md/media. Both are edits to ADR-030, which is still proposed, so they are settled when it is reviewed, not here.
D11 — stack is not named
It is default block flow. Naming the null case invites stack-tight and stack-loose, which is rule 2's combinatorial explosion arriving by the back door. SPEC-161 D6's block layout is therefore named lead, after the one thing that distinguishes it.
D12 — a composition declares sequence in its definition, not its template
A composed rune's frontmatter takes sequence (numbered | connected | plain), and the generated config carries it.
- Why the frontmatter.
sequenceis identity: whether an ordinal is information is a fact about the content (SPEC-158 D2, ADR-030 rule 3). ADR-045 and SPEC-177 let a theme or site replace a composition's template while keeping its frontmatter. Asequencewritten in the template would travel with the template, so a theme's replacement could delete the step numbers, which is rule 3's founding example. - The template marks where. The
{% group layout="ladder" %}is where the definition's sequence is stamped. - A definition that declares
sequencemust place a ladder, or it is rejected at schema construction, naming the rune. A sequence with nowhere to go is the silent loss this guards against. - SPEC-177's checks apply the same rule to a replacement template: one that drops the ladder group of a definition declaring a sequence is rejected when the site loads.
D13 — ordinals arrive as nodes, and the per-rune counters go first
WORK-583 (ready, from ADR-029) emits a numbered sequence's ordinal as a DOM node instead of a CSS counter: information belongs in the document. Composed ladders should get node ordinals from the start rather than be migrated afterwards, so WORK-583 lands before or with D2. BUG-024's removal of the four vestigial per-rune counters is the "proved by deletion" step this spec asks for, and goes with it.
D14 — a ladder stamps its own list, not every list inside it
annotateSequence recurses through every descendant of a rune declaring sequence and stamps every <ol> it finds. For a ladder that would include an ordered list an author wrote inside a step's body, which would then be numbered as steps. The ladder's data-sequence is set on the ladder element only. Whether native runes already mis-stamp nested author lists is checked during implementation and filed as a bug if they do.
Implementation notes, deliberately not yet work items
- WORK-583 and BUG-024 first (D13).
- The
{% group %}template tag withbarandladder, and the unsequenced ladder rule (D2, D3). sequencein the definition's frontmatter, and the ladder requirement (D12).- A composed
stepsover it, compared with the plugin (SPEC-161 D5). TrackadoptsmetaFieldsandblocks(D7), deletingtrack.css's hand-written push.- The five hand-written list resets that remain after BUG-024 are re-expressed through the ladder or the dimension, with their CSS deleted.
Non-goals
grid,split,treeor any topology beyondladderandrow(D1)childDefaults, the per-instance cascade for author-written children (D8)- Promoting
layoutto a universal attribute: every universal today is decoration, so a structural one is a category change that needs its own argument - SPEC-145 D15's several templates, which the variants section describes but does not specify
- Retiring
baras a rune; it stays as the author-facing row {% group %}as an author-facing rune (D2)
Acceptance Criteria
- A composition template can place
{% group name layout="bar" %}and{% group name layout="ladder" %}; both render withdata-nameand nodata-rune, and an unknownlayoutis rejected at schema construction (D2) - A
bargroup's child written as{% group align="end" %}rendersdata-align="end"and pushes to the end (D2) - A ladder renders
<ol>when the definition declares asequenceand<ul>otherwise, with eacheachiteration or top-level node in an<li>(D3) [data-zone-layout="ladder"]resets the list and flows items in a column in skeleton;cast'slistmode uses it and its own reset is deleted (D3)- A composition's frontmatter accepts
sequence; the generated config carries it, and a definition declaringsequencewithout a ladder group is rejected, naming the rune (D12) - The ladder's
data-sequenceis set on the ladder element only; an ordered list in an item's body is not stamped (D14) - WORK-583 and BUG-024 are done before or with the ladder (D13)
- A composed
stepsplaces a numbered ladder and matches the plugin's output, every difference explained TrackdeclaresmetaFieldsandblocks, its byline variant is avariantsentry overlayout, andtrack.css's hand-writtenmargin-left: autois gone (D7)refrakt contracts --checkandnpm run seo:baseline:checkreport no unexplained drift, on both contract copies- The CSS-coverage test's known-gap sets shrink rather than grow
- The authoring guide documents
{% group %}and states D9's rule
References
- ADR-030: the rules this spec is the "later spec" for; still
proposed, and the home of D10. - SPEC-158: shipped D4 and D5.
- SPEC-169: D4's
stepsexample, and D6, which routessequencehere. - SPEC-145: D19 (grouping), D32 (passing down), D15 (several templates).
- SPEC-177 and ADR-045: template replacement, which D12 guards against.
- WORK-583, ADR-029: ordinals as nodes.
- BUG-024: the vestigial per-rune counters.
- SPEC-161: D5 (
steps), D6 (lead). - SPEC-080:
metaFields,blocks,LayoutPrimitive. - SPEC-091:
variants. - SPEC-146: the boundary that makes D9's hazard silent.
- SPEC-155: the media audit, and the two rune-specific row implementations.
- SPEC-159, SPEC-171: the cascade, D8.
- SPEC-100, ADR-018:
carousel, half of D10.