de1eb7e406ea7fb1bf8b09c16873c4fce2f2eb62
17
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
76aaf06c0b |
Add SizeRule::{Min, Max, Clamp}, which the oracle refuses
`MaxSize` on the app's pin narrows the box it asks its child in and cuts the
answer to the cap; nothing on this branch does either, so the capability is
missing rather than merely unported. This is that capability as a rule beside
the widget, the way `Exact` already is: `Min(Len)`, `Max(Len)` and
`Clamp { min, max }`, resolved against the rel base a declared length is a
fraction of, and never carrying `leftover` -- a cap containing a share admits
several self-sizing fixed points (`docs/LAYOUT.md`, failed hypotheses).
Where it stands: every hand-written test passes, including the capability the
app actually used -- `a_capped_scroll_takes_its_viewport_from_the_cap` puts
400 px of content under a 100 px cap and gets a 100 px viewport with 300 to
scroll, which is what `MaxSize` gave. The 400-seed depth-5 scan does not
pass, and the reason is a design question rather than a slip, so this sits on
its own branch instead of in #19.
What the scan finds: a bound is the first rule whose effect depends on the
box its parent gives it, and the retained machinery hands a widget a box by
paths that never ask it again -- `place_in` from a re-placing parent, and
`reposition` after a parent's box moved. A decision made when the box was one
length therefore survives into a box of another, so warm and cold disagree
about a tree they agree on structurally. Four readings were measured over 400
seeds at depth 5:
- deciding at every ask and keeping it: seeds 291, 1, 120, 178, 64 differ.
- the same, re-decided at `place_in` too: seeds 1, 362, 188, 254, 156 differ,
because that path's box is the one the answer chose rather than the one the
widget was asked in.
- skipping a place its parent decided outright, which is the rule the share
follows: worse -- the same widget then gets two decisions by two paths.
- the bound as an answer rule only, leaving the box alone: seeds 4 and 196,
and those are the closest to passing by a wide margin.
The share is the one existing rule of this kind and it is stable because
`place_at` re-asks a child whose rel base it narrows, and because its
decision is baked into the retained place as a `Sized` length. Neither
protection generalises: a bound that binds is a length of the rel base, and
`Sized` cannot say "this slot, narrowed" for a `Within` place.
Also here, because a bound needed them: `Len::longer_than` and
`Bound::outside` share one comparison with the span; a rule that is a
fraction now pins its rel base whether the fraction is a length or a bound,
which was a real gap for `Exact` too; `widget_trait!` passes attributes
through, so the methods it defines can carry doc comments (none could);
`From<N> for Len`, so a bound reads `max_width(300)`; and `random.rs` grows
all three variants, with `describe` printing them so a failure can be written
out by hand.
Format, clippy with and without layout-diagnostics, and the suite (142 + 19 +
13 + 4) are clean. The fast ten-seed oracle passes; the long scans do not.
Neutering the bounds in the generator while leaving its draws in place puts
the same shapes back to green, so the divergence is the bounds and not the
new trees.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
0d0326769c |
Ask the root the way every other widget is asked
The root had a layout path of its own: `root_layout` read its declared
lengths against the window, while every other widget's box came of
`Painter::widget_at`, where a rule of the widget's own -- a share with pixels
or a fraction beside it -- is compared against the offer and can take the box
past it. So a share on the root was the window whatever it asked for, which
`docs/LAYOUT_LOG.md` recorded as a gap rather than fixing, and any later rule
that reads the offer would have had to be written twice.
There is one box nobody drew, and that is the whole of what the root is
asked in. `Placing::WINDOW` says it -- the full output, fractions of the full
output, no move entry and no mask -- and `Placing::ask` is then the one place
a box is decided, called by the painter, by a local redraw, and by the root's
first draw. The root's own path is what is left of it: a widget with no
parent keeps different bookkeeping, not a different layout.
Measured in a 400 px window, a probe under each of three parents, which now
agree on every row where two of them agreed before:
rule as root wrapped in a span
leftover(1) 400 400 400
px(50) + leftover(1) 400 400 400
px(500) + leftover(1) 500 500 500 (was 400 as root)
rel(0.5) + leftover(1) 400 400 400
rel(2.0) + leftover(1) 800 800 800 (was 400 as root)
px(500) 500 500 500
rel(0.5) 200 200 200
The comparison is kept on the widget asked about rather than on the asker,
which is what makes the root need nothing of its own: a window range means
the same thing at either end of an ask, `in_parent` passes one up unchanged,
and the asker ends up holding it through the child's drawing exactly as it
did when `longer_than` narrowed the asker directly. The root has no asker, so
its own record is the only place that range can live -- and `resize` already
checks that record, so a share crossing its length is caught with no new
code. `a_share_past_the_box_is_decided_again_on_either_side_of_the_crossing`
now runs at the root too: 500 at a 400 window, 900 at 900, 500 again at 400.
Two things this changes beyond the share. `DrawInfo::asked` is now the place
the parent offered rather than the place the ask came to, so a local redraw
re-decides the rule instead of re-reading the decision -- the two were the
same until a rule could move the box. And the root's `is_region_node` is
read, where the old path passed `false`: a region-node root now gets its
entry, whose translation is the identity, pinned by
`a_region_node_root_is_a_region_node`.
Format, clippy with and without layout-diagnostics, and the suite (136 + 19 +
13 + 4) are clean. The cold dump over 400 depth-5 trees is byte-identical to
`2dba90b` across all 34,571 boxes, and the three seed scans pass: 400 at
depth 5 (61s), 1,000 at depth 6 (155s), 2,000 at depth 4 (291s).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
445287c95c |
Ask a value a question through a reference
A method taking `self` can only be called on a value, so anywhere the caller
holds a reference it has to dereference to ask -- which costs the caller
whether or not the type is `Copy` (Bryan, 2026-09-20, correcting the opposite
change made in
|
||
|
|
750217631d |
Drop three things nothing reads, and say what a span makes scalable
`Axis::pair` and `RegionAlign::NEAR` arrived on this branch with no caller and never got one. `Holds::contains` took `&self` where its five siblings on the same `Copy` pair of pixels take `self`. The comment beside a span's cross-axis accumulator said a scalable child "makes Children scalable too"; `Children` names nothing here, and what it makes scalable is the span. |
||
|
|
55df32a33c |
Say layout's operations by name, and index a pair by its axis
Four rounds over the same idea: an expression that needed a comment to say
what it computed wanted to be a named operation.
The placement description is built by chaining off the value that says it.
`UiSpan::within_desc`/`shifted_desc` and `Len::as_desc` replace the
`PlaceDescAxis::` constructors, `PlaceDescAxis::axis` lifts one axis into a
pair with the whole box across it, and `PlaceDesc::per_axis` covers the case
where the two axes differ. `beside` is dropped: `from_axis` already said it.
Seven module-level functions become methods on the value each took first --
`Widgets::declared_lens`, `LayoutLen::fills`, `PlaceDesc::placement` and
`::rel_base_and_region`, `Size::within_box`, `UiRegion::at_origin` and
`::as_translation`.
`UiSpan::place` is the aligned-placement rule, which was written out three
times; `LayoutLen::without_leftover` is the sibling `apply_leftover` never
had, at six sites; `is_px` and `is_only_leftover` name field comparisons the
surrounding comments had to translate; `Holds::covers` was interval
containment spelled out by hand. A span's `shared` loses the two arguments
that did not vary across its loop.
`LayoutHolds` was four two-element arrays where every other pair here is a
struct of two per-axis values, so nothing it did could be written once.
It becomes `AxisHolds` on `x` and `y`, and `and`, `covers` and `contains`
lose their loops.
Every pair gets `Index<Axis>`/`IndexMut<Axis>` through one macro, and the
eighteen `axis`/`axis_mut` methods go. `const_index` keeps the accessors
usable in const context.
Cold layout is unchanged: `layout_dump` over 400 depth-5 trees is identical
to
|
||
|
|
c330ecec2b | Skip inverse arithmetic for unrestricted layout validity | ||
|
|
32542d0c0b |
Thread a box in pixels down the draw, one multiply from its parent's
A box in pixels was composed back up the move chain, on a grid fine enough that the walk rounded once, while a widget's offer was threaded down through its ancestors' offers. Two routes to one length, which is what `Holds::through` allowed for -- and the offer's route broke at a region node. `offered_region` fell back to `UiRegion::FULL` there, and `redraw` resolved that against the node's slot entry, which holds the box its parent *placed* the node in. Under a `Scroll` that is as long as the content rather than the viewport, so everything below was re-asked at a width its own answer had produced and the old answer confirmed itself: shrinker seed 220 on `reorder` left a widget 290px out. `ActiveData` now keeps a widget's box as lengths of its parent's box -- `given_len`, and `offer_len` for the box it was first asked about -- and `DrawInfo` carries the pixel lengths, threaded down one `Len::to_px` at a time: the box its parent gave it, then the part of that box its own answer placed the drawing in, which `placed_lens` states once for both `placed_box` and the walk. `Painter::px_size` and `px_len` read that value, and `UiRenderState::asked_px` takes the same steps back up the parent chain where a local redraw starts part-way down the tree. Neither chain has a coordinate frame in it, so neither can break at a region node, and warm and cold reach every length by the same expression. Three things follow. `Holds::through` is the exact preimage of `px + floor(rel * box)` -- two divisions, no allowance, the whole of a box mapping back to itself. A local redraw asks in the box its parent gave it and only where that box is as long as the offer, which retires `redraw`'s third ask and the region-node exception beside it; `draw_inner` places the answer inside that box itself. And symbolic regions are left to the GPU, hit testing and remaps, where `Moves::resolve` is the only walk: `wide.rs`, `Moves::compose`, `Moves::size_of`, `px_of`, `px_region`, `offered_region` and `slot_wide` are gone, 252 lines of `core/` net. `px` is deliberately not stored beside those lengths. A resize every widget's `Holds` admits redraws nothing, so a stored pixel length would be stale on every widget in the tree with nothing on it to say so, and refreshing it costs a walk down every reused subtree on the resize path. Instructions:u, medians of 21 runs, seed 1 at depth 8: | phase | before | after | | | --- | ---: | ---: | ---: | | `cold`, 200 frames | 313.1M | 312.9M | -0.04% | | `resize` | 408.1M | 405.6M | -0.61% | | `many` | 1,924M | 1,756M | -8.75% | | `scroll` | 357.3M | 323.4M | -9.49% | | `repaint` | 363.3M | 315.4M | -13.18% | `cold` and `resize` have all twenty-five work counters identical, so those two rows say the draw path costs the same threaded as composed. The other three do less work: `repaint` goes from 23 draw requests and 13 widget draws a frame to 1 and 1, `scroll` from 20 and 11 to 8 and 2, `many` from 273 and 186 to 207 and 157. Primitive writes are unmoved in every phase. Verified: `view`, `minimal`, `random`, `tabs` and `text` render byte-identical at 1920x1200 against `5b78002`, as does the `tabs` touch replay before and after the gesture, and a live resize of `random` to 1280x800 is identical both to the old head's and to a cold render at that size. The oracle passes 100 seeds in release and 120 in debug -- the debug run is the one that exercises the `Holds` assertion -- and the fifteen shrinker cases pass at 400 seeds of depth 5 and 1000 of depth 6. Seed 220 is `unsettled::a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered`, which needs both halves of this to fail: the old chain with the old allowance passes it, and the old chain with the exact preimage does not. `AGREE_STEPS` stays 2. One step passes the 100-seed oracle and fails the 400-seed shrinker on `resize-size` by 0.002 px, so what is left there is the resize path re-expressing a part as a fraction of a box that changed length, not a length reached two ways. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
45a717695b |
Compose a box down its chain once, not once a level
Bryan's call, 2026-09-16, for correctness. `Moves` walked the move chain in `Len`, so every level's four multiplies landed back on the grid before the next started and the residue grew with the depth of the tree. `WideLen` carries a length through the walk on a grid twenty-four bits of a box and twenty-two of a pixel finer, and rounds once at the end. What it buys, measured rather than argued: `Holds::through`'s allowance for the two routes to a length drops from three half steps to two, and the whole of a box now maps back to a range one step wide rather than one step per level of nesting. One half step further is arithmetically available -- the `Holds` assertion is quiet there and the whole-box case becomes an exact identity -- and it is **not taken**, because shrinker seed 220 then lays out differently warm than cold. Too narrow is meant to cost a redraw and no more; there it re-breaks a wrapping text, whose reported width moves a `Branch` onto its other subtree. That is the unsettled-text family, and closing it is what would let this go lower. The note is in `through`. `Moves` now answers three questions instead of one, and they are different questions: `size_of` for how long a box is, which is what reads a box in pixels; `compose` for where both of its ends are, which is what compares two boxes; and `resolve`, unchanged, for the `Len` walk the vertex shader does again in floats. A length composes on its own in two multiplies a level rather than four, since where the parent sits falls out of the difference -- which is most of why this is not slower. Measured on the fixed-shape fixture, seed 1 depth 8, 500 frames of `many`, medians of 25 runs with all twenty-five work counters identical between the two: 1,880M instructions and 755M cycles against 1,908M and 760M. So it is free, and a little better on instructions. Three things were tried on the way and two kept: composing the length alone rather than both ends (-111M instructions), taking the pixel term's fraction on the ordinary grid so it stays in an `i64` (-2M instructions, -8M cycles), and skipping a parent that spans its own box, which **cost** 18M instructions and is not here -- the same verdict a short-circuit got in `UiSpan::within`. Checked: fmt, clippy, 83 suite tests, 17 core unit tests, the release oracle at 100 seeds and at 1000 seeds of depth 6, all fifteen shrinker cases at 400 seeds of depth 5, and `tabs`, `view`, `minimal`, `text` and `random` byte-identical at 1920x1200 against `d21a215`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
38eba543f6 |
Tighten a validity range to what the arithmetic needs
`Holds::through`'s allowance for the two routes to a length was four half steps either side, from a derivation that said each rounding now drops a whole step where it used to drop half of one. That overshot: three is the floor, two fires the `Holds` assertion in `draw_at` on eleven generated cases, and four was never measured as necessary. Tightening both ends did not move one of the rig's twenty-five work counters, so the extra half step was not buying any reuse either. It cannot go to zero. The range has to contain the box a drawing was made in, which the assertion checks, and it must not contain a box the drawing does not hold for, which the warm-against-cold oracle checks -- and those two only coincide where a length reached two ways is the same number. It is not, yet; composing in `i64` and narrowing once is the queued change that would make it so, and shrinking this allowance is how to tell whether that worked. Checked: fmt, clippy, 81 suite tests, 17 core unit tests, the release oracle at 100 seeds and at 1000 seeds of depth 6, all fifteen shrinker cases at 400 seeds of depth 5, and `tabs`, `view`, `minimal`, `text`, `random` and the tab replay byte-identical at 1920x1200 against `2bc6bdf`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
08c9d5aa32 |
Drop a multiply to the step below rather than rounding it
Bryan's call, 2026-09-16, taken for the cycles: a share now lands a thousandth of a pixel short of its row instead of on it, which is less than an even number of pixels draws. `Fixed::mul` is a widening multiply and a shift, with the sign branch and the half-step add gone. The two short-circuits priced against the old multiply go with it: `UiSpan::within`'s test for a span that is the whole of its parent, and `Fixed::scaled`'s test for nothing scaled by something, which was the whole of `scaled` -- both cases come out of the truncating multiply unchanged, and the bodies the comparisons cost were what kept the inliner from taking `within` at all. `nm` is the check: `<UiSpan>::within` is a symbol in the rounding head and in neither the float head nor this one. `Holds::through` inverts the multiply, so its widening is re-derived: each rounding now drops a whole step where it dropped half of one, which doubles the allowance for the two routes to a length, and the multiply on the way in drops only downward, so its own step goes at the top of the range alone. The derived allowance for one truncation either side is measurably too narrow -- it excludes boxes drawings were made in, in eleven generated cases -- because each route is a chain of multiplies rather than one. Measured on the fixed-shape fixture (`Edits::fixed_branches`), seed 1 depth 8, 500 frames of `many`, medians of 25 runs of uninstrumented release binaries with this VM's garbage `perf` readings dropped: | | instructions | cycles | IPC | | --- | ---: | ---: | ---: | | `5ed9e87`, the float head | 1,761M | 688M | 2.561 | | `60367d8`, rounding | 1,915M | 777M | 2.465 | | this | 1,800M | 715M | 2.516 | -6.0% instructions and -8.0% cycles against `60367d8`, whose twenty-five work counters are identical to this one's, so that pair is the same work at a different speed. It leaves +2.2% and +3.9% against the float head, from +8.7% and +12.9% -- but the float head draws 100 widgets to this one's 97 and writes 4,272 primitives to 3,951, so that pair is not, and the remainder is not all arithmetic. Checked: fmt, clippy, 80 suite tests and 18 core unit tests, the release oracle at 100 seeds, all fifteen shrinker cases at 400 seeds of depth 5 (seed 288 on `region-node` still failing, unchanged), and depth-6 oracle seeds 18 and 190 passing with 326 still failing. `view`, `minimal`, `text`, `random` and the tab replay render byte-identical at 1920x1200; `tabs` differs on 4,664 of 2,304,000 pixels, single-pixel-wide runs along 80 columns of one band of rounded rects, which is an antialiased edge moved less than a pixel. Three tests say what changed rather than being relaxed: a multiply drops on both sides of zero, a division cannot put back what it dropped, and an unevenly nested row's shares stay contiguous and end at its edge with each edge on the even division or one step below. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
60367d806e |
Do not widen a validity range where nothing rounded
`Holds::through` inverts `px + rel * box`, and allowed three half steps either side: one for that multiply's rounding and two for the difference between a length composed down the chain and the same length measured against the window. The whole of a box has no multiply in it -- `rel` is one and taking the pixels off again is exact -- so the first half step was being allowed for a rounding that did not happen, and it compounded: a chain of widgets each taking the whole of its parent grew the interval half a step a level. Traced while making the multiply truncate, where the same compounding moved the interval off the box the drawing was made in and fired the `Holds` assertion in eleven generated cases. A range wider than what a drawing holds for is one that admits reusing it where it does not hold, so this is the unsound direction to be loose in. Checked: fmt, clippy, 80 suite tests and 16 core unit tests, the release oracle at 100 seeds, all fifteen shrinker cases at 400 seeds of depth 5 (seed 288 on `region-node` still failing and unchanged by this), and `tabs`, `view`, `minimal`, `text`, `random` plus the tab replay byte-identical at 1920x1200. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a8898aaa54 |
Give a length with no share in it its own type again
`UiScalar` was `Len` without the `leftover` weight, which is the separation canonical `main` already had as `Len` beside `LayoutLen` and this branch collapsed. It is needed back for the queued clamp: a cap may not contain a share, because a cap has to read the report a rule otherwise makes moot, and a share puts the container's division into the same equation -- two self-consistent assignments, which is the multiple-fixed-point failure generated seed 13 punished for orthogonal sizing. `min(report, cap)` is not a `LayoutLen` either: it is a sum of parts, and the smaller of two of them is not one. So `UiScalar` is `Len`, what was `Len` is `LayoutLen`, and the two say in their docs which is which: a `Len` is pixels plus a fraction of a box -- a position being the length from the box's start, which is why a span is two of them -- and a `LayoutLen` is a `Len` plus a claim only a container dividing its room can answer. `From<Len> for LayoutLen` is the one-way step between them. Names only; the shader's `UiScalar` is renamed with them. Checked: fmt, clippy, 105 tests, and `tabs`, `minimal`, `view`, `text` and `random` byte-identical at 1920x1200. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f11f5f4825 |
Divide twice in a range's inverse, not four times
Which end of the answer each bound comes from is known from the sign of the fraction before dividing; taking the min and max of four divisions asked the question twice. A division is the most expensive thing in that function and it runs per child per axis. `many` 0.283 ms a frame to 0.278. Small, and strictly less work. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
39e4ca20e6 |
Decide layout on the grid end to end, and delete the tolerance
`Px` and `PxVec2` reach the last places a pixel was a float: the window, the box a widget reads, the box it is compared against, and `PixelRegion`. A pointer, a wheel notch and a shaped glyph advance still arrive as floats, and each is put on the grid where it arrives. `Holds` is an interval of `Px`. `HOLDS_EPSILON_PX` is gone with the `exact`/tolerant split it existed for: `at` is the length a widget read, an open end is the next step along, and `same_px` is equality. `Span`'s margin from `5ed9e87` goes too -- the box a parent hands back and the sum of what its children asked for are counts of the same step, so the boundary decides the same way from either side. Three things had to be true for that, and were not: `Holds::through` inverts `px + rel * box`, which rounds -- so a part of a given length came from a range of boxes, and inverting the length alone gave a point that need not contain the box the part was drawn in. It now maps the half step either side, and one more for a length composed down the chain against the same length measured against the window. `RegionRemap` translates when a box only moved, rather than dividing to find each part's fraction and multiplying to place it again. Two roundings landed a step from where growing the tree that way does; a move is exact on a grid, which is the whole reason `tests/drift.rs` was written. A pixel is `1/1024` rather than `1/64`. At `1/64` the residue of a length reached two ways was one step, and one step was 0.016 px -- enough to move a box. `PX_SHIFT` and `REL_SHIFT` are the only statement of the grid now, and the shader's copy is prepended from them rather than written twice. Checked: fmt, clippy, 102 tests, 100 generated seeds in 75 s, all five shrinker cases at 300 seeds, and `tabs`, `view`, `minimal`, `text` and `random` byte-identical at 1920x1200. What the fuzzers ask for is now a step, not a twentieth of a pixel: the shrinker's five cases agree within one (`resize` exactly), and the oracle's two-operation cases within two. The residue is a single rounding either way -- it scales with the grid rather than accumulating, which is why it is a thousandth of a pixel now. Closing it means one way of asking how long a box is, rather than a chain composed down and a length measured against the window; that is a bigger change than this one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4e28f1047e |
Put positions on the grid, and decode them in the shader
`UiScalar` is `Rel` beside `Px` rather than two floats, so composing a position down a chain of boxes adds exactly and rounds only at the two multiplies `within` makes. `UiSpan`, `UiRegion` and `UiVec2` follow it, the hand-written `Hash` goes away with the bits it hashed, and `impl_op!` grows a `same` form for a type whose fields are not the same kind of number. `Len` is still floats, so the seam converts: `Px::from_f32` where a span adds a child's length to its cursor, and `to_f32` where something outside layout wants pixels. Those go when `Len` follows. The GPU reads what the CPU wrote: the instance attributes are `Sint32x2` and the shader decodes by `1/64` and `1/2^24`, both exact in `f32`, then composes the move chain in floats as before. It has to agree with itself frame to frame rather than with the CPU to the last bit. Two things fell out of making the numbers exact. `floor` at the rasteriser was picking the pixel below wherever a fraction divided a window exactly. A fifth of 1920 is 383.99998 through a rounded `Rel` -- and was 384.0 through an `f32` that happened to round up -- so five tabs each lost their last column. `snap_floor` takes a coordinate within half a step of a boundary to be on it, which is the same rule as everywhere else here: decide where values do not land. A widget measured on one layer and drawn again on another kept the first layer, because `try_reuse` compared everything about a retained drawing except which list it sits in. `Stack` does exactly that for its background, so every panel's text went under its own background. It only worked before because the two asks differed by a rounding and forced a redraw; `tests/retained.rs` pins it now, and `ReuseOutcome` can say `WrongLayer`. Checked: fmt, clippy, 100 tests, 100 generated seeds in 70 s, all five shrinker cases at 300 seeds. `tabs`, `view` and `minimal` render byte-identical at 1920x1200; `random` differs in 36 pixels by one level; `text` differs where glyph origins moved onto the grid -- same positions, same spacing, different subpixel coverage, checked at 6x against the old render. Measured on the way: with the fuzzer comparing for *equality* rather than within 0.05 px, `resize`, `repaint` and `size-change` already pass 100 seeds. `reorder` fails one seed by exactly one step, which is the `Len` seam above. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
71c9c39523 | Replace placement calls with region nodes | ||
|
|
29c7881c8a | Track retained layout validity explicitly |