Replace the step 3 plan with the one-ask protocol and record why it failed

The handoff now describes wip/one-ask at 3091fb8: a widget draws once in
the box it is asked in, its answer is placed by re-expressing the drawing,
and nothing is drawn again in a box an answer chose. The log records what
the step 3 plan got wrong -- it kept the second draw whose measurement
bit could not be defined, demanded a contract of the answer box, and
narrowed frames by region -- and what fuzzing the new protocol found: the
Part::Of composition dropping a pinned length, and a local redraw that
must be put back in the parent's answer box. The Pad frame rule is left
as a decision for Bryan with its cost stated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
iris-aiandClaude Fable 5.1 committed 2026-09-18 18:23:49 -04:00
1 parent 39978ff8ea
commit ef324aa88b
3 files changed
+370 -319

No files matched your search

+106 -16
View File
@@ -7,9 +7,91 @@ must outlive it (settled design, the measurement method) is already in
`docs/LAYOUT.md`, and the current plan is in `docs/HANDOFF.md`. Commit ids
are in `/home/bob/repos/iris-layout-experiment` unless said otherwise.
## The two open shrinker seeds on `wip/transparent-frames` (planner, 2026-09-18)
## What the one-ask protocol found (planner, 2026-09-18, third session)
Both are the rule in `draw_inner` that decides which answer places a widget's
Branch `wip/one-ask` at `3091fb8` over `4328eac`. The change is described in
`docs/HANDOFF.md`; this is what building and fuzzing it turned up, in order.
- **The step 3 assertion was the wrong check.** At the worker's stopping
point every generated case stopped on `assert_eq!(extent, info.part)` in
`place`, on a `Text` asked at 45 px in `Within(All)` whose longest word is
89.5 px: its drawing holds for `[45, 45]` and its answer box is 89.5 px
wide. No leaf can promise its drawing holds for a box chosen from its
answer; the plan's sentence "every drawing must hold for the answer box it
supplies" was the defect, not `Text`. Removing the check *and the redraw
it guarded* -- placement is re-expression, unconditionally -- made the
suite green including both decided-box pins, with nothing else changed.
- **Six retained tests encoded the paired draw** (`settled + 2`, "drawn
once" expecting two): they now expect one draw. Two more failed because
`Stack` had been changed to ask non-sizing children with `Fill(All)`; they
need `Within(All)` so a smaller answer sits inside the stack, with the
frame narrowed to the sizing answer. That is the whole of the suite churn.
- **Seed 2 at depth 4 (`repaint-some`, `region-node`)** shrinks to a `Stack`
sized by one text. A local redraw of the text asked it again in the box
the stack was *asked* in and left it filling that; cold puts it in the box
the stack's *answer* chose. A local redraw must put the fresh drawing
back at the retained place of the parent's answer box, always, not only
when the asked and placed places differ.
- **Six seeds at depth 5** (60 `repaint`, 308 `every-size`, 20
`region-node`, 248, 384, 162 `reorder`) shrink to one shape: a zero `Pad`
round a widget reading `extent_len` (a span with a share rect, or a
`Branch`), as a share child of a span beside a fixed sibling. Seed 248
reproduced: **cold** was wrong. In the measuring pass the inner span read
`far = rel 1 - 17.6px` and wrote slots in it; in the placing pass the pad
got a narrowed frame of the same pixel length, was reused (its contract
had no pin, because `in_parent`'s `Part::Of` arm drops a child's
`extent_len`), and its child was re-expressed into a frame where `rel 1`
means 17.6 px less. The inner span's own record had the pin; only the
composition lost it. Composing the pin through `Of` -- exactly where the
part is the whole box less pixels, `pinned - part.px`, and pinning the
parent's own length otherwise -- fixed all six; the planner's earlier
version of the same experiment broke seeds under the old protocol because
the second draw was still there.
- Cost after the fix (widget draws / distinct widgets, depth 8): seed 1
cold 331/288, many 78/78, size 3/3, scroll 1, resize 40/15; seed 13 cold
1179/982, many 335/333, resize nothing drawn. The table with the baselines
is in the handoff. `many` at seed 13 touches 333 distinct widgets because
a share child is asked twice per span draw, and `resize` at seed 1 draws
40 because positive-direction spans without shares pin `far`; both are
step 4 there.
- The 1000-seed depth-6 oracle and the 2000-seed depth-4 scan were started
at `3091fb8` and had not finished when this was written; the next session
reads `/tmp/oracle-1000-6.log` and `/tmp/shrink-2000-4.log` if they still
exist, or reruns them.
## What the step 3 plan got wrong (planner, 2026-09-18, third session)
Three things, each a different kind of mistake:
1. **It kept the question the machinery could not answer.** The offer bit
existed because a widget was drawn twice, once where asked and once where
its answer placed it, and something had to say which draw was the
measurement. Both plans of 2026-09-18 tried to define that bit better
(the worker's four rules, the planner's two experiments). The repair is
to have one draw, after which the bit has nothing to name. A plan that
proposes bookkeeping for a distinction should first ask whether the
distinction has to exist.
2. **It stated the contract at the wrong box.** "Every drawing must hold for
the answer box it supplies" reads as a tightening and is actually
unsatisfiable for any widget that reads its box. The right statement is
the opposite: the answer box is never a question, so nothing is demanded
of the drawing there; `Holds` is about the ask box only.
3. **It narrowed frames by region.** The worker's share frames were
`extent_part(axis, From(span))`, a position in the span's frame; a frame
given as a position does not move when its part moves, and `re_ask` had
no way to put it back. A narrowed frame must be a *length* of the
parent's frame, placed into the part by alignment on every placement,
exactly as a declared rule already was.
The requirements did change under the plans (transparent frames, then
shares narrowing frames, then `Scroll`'s content length), which explains
some churn but not the three above; those were reasoning errors that a
five-widget reproduction with the retained records printed would have
caught in minutes. Print the records before proposing the next rule.
## The two shrinker seeds that were open on `wip/transparent-frames` (planner, 2026-09-18) -- closed by `wip/one-ask`
Both were the rule in `draw_inner` that decided which answer places a widget's
box: `measured = if info.offer() { fresh } else { retained }` with
`offer() := place == offer_place`. That bit is meant to say "this ask is a
measurement" and has no consistent value once a container's body runs in
@@ -169,20 +251,24 @@ at depth 8; draw counts are deterministic so these are single runs.
`e44dea3` is #18's head, `34cafb6` the commit the branch starts from,
`49cec82` its head.
| seed 1 | e44dea3 | 34cafb6 | 49cec82 |
| --- | --- | --- | --- |
| cold | 369/261/10.6 | 463/274/13.3 | 516/288/12.0 |
| repaint | 1 | 1 | 1 |
| many | 157/95/0.33 | 263/108/0.59 | 187/119/0.52 |
| size | 16/12/0.018 | 3/3 | 3/3/0.010 |
| scroll | 2/0.002 | 1 | 1/0.004 |
| resize | 13/13/0.019 | 22/15/0.032 | 24/76/0.090 |
| seed 1 | e44dea3 | 34cafb6 | 49cec82 | 3091fb8 |
| --- | --- | --- | --- | --- |
| cold | 369/261/10.6 | 463/274/13.3 | 516/288/12.0 | 331/288/12.3 |
| repaint | 1 | 1 | 1 | 1 |
| many | 157/95/0.33 | 263/108/0.59 | 187/119/0.52 | 78/78/0.13 |
| size | 16/12/0.018 | 3/3 | 3/3/0.010 | 3/3/0.008 |
| scroll | 2/0.002 | 1 | 1/0.004 | 1/0.001 |
| resize | 13/13/0.019 | 22/15/0.032 | 24/76/0.090 | 40/15/0.096 |
| seed 13 | e44dea3 | 49cec82 |
| --- | --- | --- |
| cold | 1330/707/20.3 | 2940/982/28.3 |
| many | 524/159/1.09 | 1091/423/2.39 |
| resize | nothing drawn | 2215/510/6.56 |
| seed 13 | e44dea3 | 49cec82 | 3091fb8 |
| --- | --- | --- | --- |
| cold | 1330/707/20.3 | 2940/982/28.3 | 1179/982/24.6 |
| many | 524/159/1.09 | 1091/423/2.39 | 335/333/0.62 |
| resize | nothing drawn | 2215/510/6.56 | nothing drawn |
The `3091fb8` column is single runs from the third session; its `cold` and
`resize` at seed 1 are after the `Part::Of` pin composition, which added
18 and 19 draws respectively over the version without it.
Three findings, each applied on the branch:
@@ -268,7 +354,11 @@ is the padded share and fits. The clipped render records the current bug.
`unsettled::a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered`.
- **Four bookkeeping rules for "which draw is a measurement"** (worker) and
**the two experiments above** (planner): each fixes some seeds and breaks
others. The bit is undefinable; stop trying to define it.
others. The bit is undefinable; `wip/one-ask` removes the second draw, so
there is nothing left for it to name.
- **Asserting that a placed drawing holds for its answer box** (worker's
step 3): unsatisfiable for `Text` and for any widget that reads its box
and reports something else. The answer box is not a question.
- **Choosing between a fixed and a relative child in pixels at the span's
current width** admits multiple self-sizing fixed points; seed 13 settled
differently warm and cold under it. The same circularity is what a cap