Files
ai-app/docs/HANDOFF.md
T
2026-09-20 21:35:36 -04:00

367 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Handoff
Where the work in flight stands. The settled layout design, the vocabulary
and the measurement method are in `docs/LAYOUT.md`; what the review of #19
found is in `docs/LAYOUT_LOG.md`.
## Built-in bounds replace `MaxSize` on PR #19
Bryan clarified that the built-in attribute should replace the wrapper.
Work is in `/home/bob/repos/iris-deferred`, on `layout/one-ask`, at
`05e6ced` (pushed). Bounds now constrain the offer in `Placing::ask`;
declarations are retained as resolved window lengths, so movement does not
decide the bound again.
`.max_width`/`.max_height` now edit the same widget, like the minimum
helpers, and `MaxSize` is removed. Preferred lengths and bounds are
independent, so either order of `.width` and `.max_width` works. The app pin
is unchanged.
The review fixed three cases: `window_holds` erasing an ask's bound crossing
(seed 104, depth 5), inactive bound edits failing to update the parent's offer
dependencies (seed 144, depth 6, reduced further to three widgets), and a
fractional intrinsic cap being read against its already-allocated slot.
The latter has an absolute 75-in-300 geometry check; warm/cold agreement
alone cannot catch it.
206 ordinary and 210 diagnostic tests pass, with warning-clean Clippy both
ways (207 and 211 at `2ac0843`, less the duplicate test the sweep merged).
All three layout scans pass (400 depth 5, 1,000 depth 6, 2,000 depth 4), as
do 400 depth-5 trees in each of three deferred-request corpora. All 34,986
unbounded cold boxes match `4cb6f68`; bounded offers intentionally change.
The final unbounded-path optimization preserves every bounded cold box too.
Five-run release instruction medians show capped rows using 21.7729.64%
less CPU work than the wrapper. Plain/fixed-width retained resizes differ by
under 0.05%; forced redraws improve about 0.3%. Text workloads differ by at
most 0.13%. Independent preferences and bounds cost 32 extra inline bytes per
widget; all three allocation fixtures remain allocation-free after warm-up.
`docs/LAYOUT.md` records the contract and measurements. Artifacts are in
`/tmp/attribute-bounds/`.
## A request is held in the arena its nodes are allocated in
`05e6ced`. `SizeRequest` was a second expression shape beside the one
`RequestArena` already had -- the same three operators, the same constant fold
written twice, and an `import` that walked a pointer tree rebuilding it as
arena nodes. Bryan settled it on 2026-09-20: one node type, an arena, and no
refcount, since nothing shares a request and nothing outside widget code holds
one. A rule's expression now holds a small arena of the pass's own node type,
and `import` grafts those nodes across. A plain length stays inline, so
`size_of::<SizeRule>()` is 40 either way. `bounds_cost` `MODE=cap` is -1.34%
and `revision_cost` resize -0.79% in release instruction medians.
`docs/LAYOUT_LOG.md` has the full entry. Verified with all three seed scans,
the three deferred corpora, 206 and 210 tests, and the cold dump byte-identical
to `2ac0843` across all 34,986 boxes.
## A quality sweep of the request arena is submitted on PR #19
The twelfth sweep landed at `f48e04e`. It is the first review of `05e6ced` --
one node type for a request, an arena per owner, and `Widgets::edit_bound`.
Four findings, the largest being that the one path copying a request into
another arena was run by nothing: all 206 tests passed with a `panic!` in it,
because every expression in the corpora and in the cases compares an
expression against a plain length. A fixture with absolute geometry and a
fourth arm in the deferred corpus cover it now, and the path was already
correct. The other three: `Debug` on a request still printed the derived
arena its own new `Display` was written to avoid, a method asked nothing of
its receiver, and a closure parameter shadowed the rule it was called on.
`docs/LAYOUT_LOG.md` has all four, with the four rules it tripped and left.
Verified: format, clippy both ways, 207 and 211 tests, the cold dump
byte-identical to `05e6ced` across all 34,986 boxes, and 400 depth-5 trees in
each of the three deferred corpora in 200.95s.
## A quality sweep of the bounds work is submitted on PR #19
The eleventh sweep landed at `ea1f836`. It is the first review of `2ac0843`
-- `SizeRule` from an enum to a preferred length beside an independent bound,
the offer constrained in `Placing::ask`, and `MaxSize` removed. Eight
findings, the two largest being a hint read that answered "cannot say" above
the diagnostics and so counted as no read at all, and five spellings of
reading one environment variable across the rigs, which is the class the
tenth sweep found one commit earlier -- there is now one `env` in
`tests/rig/`, used by all six. `bounds_cost` also verified 128 regions inside
its measured loop; the check moved out, and the 0.65% it measured is written
beside it. `docs/LAYOUT_LOG.md` has all eight, with the two rules it tripped
and left. Verified: format, clippy both ways, 206 and 210 tests, 400 depth-5
trees warm against cold in 64.19s, and the cold dump byte-identical to
`2ac0843` across all 34,986 boxes.
## A quality sweep of the deferred system is submitted on PR #19
The tenth sweep landed at `4cb6f68` in `/home/bob/repos/iris-deferred`. It is
the first review of `76aaf06` through `0e838e9` -- the bound rules,
the deferred request path and the invalidation fix. Ten findings, the largest
two being a bound's held length looked up a second time through a value that
could not promise the end it named (`Bound::at` `expect`ed it), and a bound in
pixels pinning the rel base it is not read against. `docs/LAYOUT_LOG.md` has
them all, with the two rules it tripped and left and two corpus questions for
Bryan. Verified: format, clippy both ways, 197 and 201 tests, all three seed
scans (69.02s, 174.87s, 330.39s), and the cold dump byte-identical to
`0e838e9` across all 34,986 boxes.
## Performance sweep is submitted on PR #19
The performance fix is `0e838e9`. It keeps dependencies only for size requests the allocator uses, and borrows
`Widgets::size_rules` rather than cloning both axes at every lookup. Used
hints retain their reader dependency; adding a cap after layout is checked for
both hinted and measured shares. Ordinary text measurement still tracks its
own dependencies. The app's Iris pin is untouched.
Repeated release instruction counts reproduce the reported +5.9% resize cost
and find a larger defect: an unchanged paragraph repaint invalidated its span,
turning 0.376B instructions into 3.010B in a 40-paragraph, 2,000-frame fixture.
The fix is 0.370B. Text resize is now +0.99% against `de1eb7e`, the width sweep
+1.23%, and edits/scroll are within 0.3%. `docs/LAYOUT_LOG.md` has the full
performance sweep, including deep-tree regressions, RSS, upstream-base
comparison, and GPU timestamp measurements. This is not a universal speedup.
Passed: format, workspace Clippy with and without diagnostics, all 197 ordinary
and 201 diagnostic workspace tests, zero steady allocation fixtures, 400
depth-5 / 1,000 depth-6 / 2,000 depth-4 trees, 400 depth-5 expression trees,
400 relative-bound trees, GPU recording and move-chain rigs, and identical
cold geometry for all 34,986 bounded boxes against `8780b40`. Code and comments
were reviewed separately; the final test/comment refinements passed their
focused checks. Artifacts are under `/tmp/pr19-perf/`.
## Deferred comparisons are submitted on PR #19
The deferred-comparison implementation landed at `8780b40`, before the
performance correction above.
`/home/bob/repos/iris` remains on `layout/bounds`; that branch's `de1eb7e` was
fast-forwarded into #19 before this work. The app's Iris pin is untouched.
The new request path composes min/max/clamp before assigning leftover slots,
while ordinary widgets keep returning `Size`. Known requests skip provisional
painting; measured leaves can complete a nested expression. The implementation
uses a reusable expression arena and retained draw buffers. The allocation rig
checks zero allocations after warm-up for unchanged plain and clamped trees.
Generalized cross-axis maxima are deferred because hidden main-axis shares
must not contribute to them. The current design is in `docs/LAYOUT.md`.
## The Iris layout repair is submitted
**Iris PR #19** (`layout/one-ask`) replaces closed #18. Before the deferred
comparison work above, its review rounds past `cadfba0` are described in
`docs/LAYOUT_LOG.md`:
- **The repair**, `add6774` and `84dad21` -- collapsed-share placement,
retained mask ownership, a redraw-on-reparent defect, and repeated work in
the test harness.
- **The vocabulary and the container API**, `5642f20` through `58ce74d`.
- **Naming**, `55df32a` through `40b89c1`.
- **A sweep over the logic those names exposed**, `8d2b7a5` and `6c84b6f`.
- **A clarity sweep**, `3da1c71` through `1ebd4d3` -- naming the pairs layout
returns, `Span::slot`, a diagnostic that printed the rel base while calling
it the box, and `in_parent` matching a place's own cases.
- **A quality sweep**, `aea0387` through `69ba915` -- a kept contract judged
against the placed box rather than the box asked about, two things nothing
read, three reuse rejections the diagnostics could not see, and a fuzz case
that ran only in the long scan.
- **A sweep over the renderer, the text store and the retained path**,
`d8d5122` through `1096c31` -- a contract kept where the new window left
it out, a surface configured under its own texture, a counter naming the
wrong contract, things nothing reads, and a question asked through a value
rather than a reference.
- **A sweep over the shader boundary and the position widgets**, `b7b8d09` --
two constants the shader and the CPU both count in written twice, and a
`Scroll` positioning content the framework positions, which cost a redraw
at the default alignment.
- **A sweep over the rigs, `Fixed`, and `b7b8d09` itself**, `f8aa0c5` -- the
other half of that same `Scroll` test, which could never decide it; a grid
conversion and its helper that only their own test called; `Len`
arithmetic written a component at a time; a question asked through a value
one line from its `&self` sibling; and `run-headless.sh --resize` leaving
a replayed gesture scaled against the mode the output used to have.
- **A sweep over the tests and the seventh sweep's own fix**, `77ed7a2` --
four shrunk fixtures naming one widget under three names and counting
each in the list of boxes the case compares, seven copies of a helper
sitting at the top of the same file, the GPU rigs' adapter probe written
twice, a mask resolved three times, a field nothing reads, a shrinker
claim its own assertion does not make, and three stale numbers.
- **A sweep over the widget vocabulary and the eighth sweep's own fix**,
`c2b8bf8` -- a widget's own size hint overriding a length rule the hint
cannot express, marking a widget for redraw having no name at twenty-one
call sites, the helper the eighth sweep shared being unable to see the
defect it had just fixed, two bare arrays where the framework names the
pair, forty-five lines nothing references, and one word for two things.
- **A leftover as a minimum, and images in the trees**, `b295c8b` and
`2dba90b` -- a share under a parent that divides nothing losing the
overflow its pixels asked for, said as the place the parent gives and
sharing one comparison with the span; and `Image` grown in the generated
trees, which is the only widget here whose hint is a length in pixels.
- **One ask, the root's included**, `0d03267` -- the root had a layout path
of its own, so a rule that reads the box it is offered reached every widget
but that one. `Placing::WINDOW` is the box nobody drew and `Placing::ask`
the one place a box is decided; the root's own path is now the bookkeeping
a widget with no parent keeps. Bryan asked for this rather than a widget
above the root, which would pin the tree to pixels.
- **The deferred request system's own sweep**, `4cb6f68` -- described at the
top of this file and in full in `docs/LAYOUT_LOG.md`.
- **The bounds work's own sweep**, `ea1f836` -- the same, over `2ac0843`.
- **One node type for a request**, `05e6ced` -- the expression a rule holds
and the expression the pass allocates were two shapes of one thing, with
the fold written twice and an `Arc` where nothing shares.
- **The request arena's own sweep**, `f48e04e` -- the join that copies one
request's nodes into another's, which nothing in the suite ran.
The settled design of the vocabulary rounds is in `docs/LAYOUT.md` under
"Three names, and the one argument that says them". Bryan settled the API
over 2026-09-17 to 19; it is current, not frozen.
The core design remains sound. Round-to-nearest is still unchanged.
The Iris worktrees share Git storage. `ai-app-2/iris` stays on `app-pin` at
the app's `32f6ad8` pin until the integration below is ready.
### The branch layout, and the trap that used to be here
`main` is PR #19's base and tracks `upstream/main` (`iris/iris`), so the
obvious commands are the right ones:
git merge-base main layout/one-ask # ca2b4b2, the PR's base
git diff main...layout/one-ask # exactly what #19 changes
It was not always so. Until 2026-09-20 `main` tracked `origin/main` -- the
*fork's* line, which carries the app's 45 commits, is not an ancestor of
upstream's main, and sat four merged pull requests behind it (#10 parley
text, #12 pointer routing, #16 draw/size merge, #17 headless rig). Diffing
against it showed all four as this branch's work, which is how the parley
migration's undo path kept being reported as #19's; the sixth sweep lost
half a session to it before Bryan caught it, and the fourth and fifth
sweeps deleted `Painter::text_data` partly on the same false reading.
Bryan asked for the setup fixed rather than documented. What changed:
- The app's line is now the branch **`app-pin`** (`32f6ad8`), pushed to
`origin`. That is the name to use for it; it is no longer called `main`.
- `main` points at and tracks `upstream/main`.
- `.gitmodules` pins the `iris` submodule to `branch = app-pin`, so
`git submodule update --remote` follows the app's line and cannot drag
the pin onto upstream.
- `git iris-base` and `git iris-diff` are still configured and still
correct; they now agree with plain `main`.
`origin/main` on the fork was force-pushed to match, and `origin/HEAD`
follows it, so a fresh clone of the fork with no `upstream` remote gets the
right base from the plain commands too -- verified by cloning one. The
fork is managed entirely by agents and nobody works from it directly, so
rearranging its branches is always safe where it improves the workflow
(Bryan, 2026-09-20); that licence does not extend to the `iris/`
repositories, which are what pull requests target.
The submodule's recorded commit is unchanged at `32f6ad8` and stays
reachable through `app-pin`, so nothing about the app's pin moved -- only
the name it is reachable by.
### How to check a round
**Always**, because they cost nothing: format, workspace clippy under
`-D warnings` with and without `layout-diagnostics`, the workspace tests, and
the **cold dump**. `layout_dump` over 400 depth-5 trees is **34,986** boxes
since `76aaf06` grew bounds in the trees, and is unchanged through `f48e04e`.
It was 34,571 from `2dba90b`, which grew the images, and 34,488 before those; the fourth through eighth sweeps all
repeated 34,492, which is a `wc -l` of the whole run rather than of its box
lines, so count the lines that are a box (`grep -cE '^[0-9]+ [0-9]+ '`). It is the only thing that catches two
same-typed values being swapped, which is the failure mode of a rename or a
move. The repair moved 650 of those boxes, all from the collapsed-share
correction. Subsequent review-only changes preserved cold geometry through
`4cb6f68`; the offered-bound change in `2ac0843` intentionally changes bounded
geometry while preserving all unbounded boxes.
**Only when the change can alter what layout computes**: the three seed scans
-- 400 at depth 5, 1,000 at depth 6, 2,000 at depth 4. They cost about a
quarter of an hour and they exist to find logic that is wrong on some tree
shape, so a rename has nothing for them to find (Bryan, 2026-09-19). Never
start one and then edit the tree: cargo rebuilds mid-flight and exits 1 from
a compile error, which reads exactly like a fuzzer failure.
## What is next, in order
1. **Bryan's review of #19.** Fixes are themselves unreviewed code: repeat
`pre-submit-review` over each round's changes, and apply the gate above to
whatever each one touched. The ordinary oracle does not replace absolute
geometry and retained-primitive expectations. The seventh sweep is the
evidence: reading `b7b8d09`, the sixth sweep's own fix, found that it had
removed one dead operand from a test and left the one beside it.
The eighth adds a second lesson, about the tests themselves: a fixture a
fuzzer shrank is a regression test, so a round that rewrites one has to
prove the tree is the same rather than that the case still passes, since
a fixture that quietly changed still passes and covers nothing. The ninth
adds a third: where a round fixed a class of defect one instance at a
time, put the check in the shared helper, which both closes the class and
tests the instances that round said were already fine.
The twelfth adds a fourth, about coverage rather than about a defect: a
branch only a combination of the public API reaches can be reached by
nothing and still look covered, since every test around it passes. Put a
`panic!` in the arm and run the suite; it costs one build and it answers
the question the test names cannot.
2. **A review of everything written before the review gate existed.**
`pre-submit-review` and the rule that nothing is submitted unreviewed
arrived on 2026-09-13, well after the Rust port and most of Iris were
written, so all of that code went in unreviewed and none of the sweeps
above covered more than the layout branch. It wants a pass of its own
(Bryan, 2026-09-20). The surface-texture defect in `02048ea` is the
argument: nothing about that arm was hard, and it was written wrong
anyway, which is what a first reader catches and a later sweep of some
other subject does not. Three things it already has waiting, left out of
#19 because they are outside its diff: `Align::tuple` and both
`partial_align`s have no callers, `Vec2::align`/`partial_align` are
`UiVec2`'s with a conversion in front, and `impl_op!` carries four
grammars of which `core/src/util/vec2.rs` uses two, one line apart.
`Align` now has `Index<Axis>`, so the `if let Some` each
`partial_align` writes twice collapses when that sweep reaches them.
3. **Integrate the app's Iris capabilities before changing its pin.**
`32f6ad8` has 45 commits not reachable from the review branch; shared UI
ownership, richer masks, Android support, and app-side performance work
must survive the integration. What has to survive is those capabilities,
not the calls the app makes today: the app is to be largely rewritten
against the new API rather than ported call by call, so nothing in Iris
is kept alive for the app's sake (Bryan, 2026-09-20).
4. **Round-to-nearest**, CPU and shader together as one verified change.
Bryan approved it on 2026-09-17 and neither half has landed; the
derivation, the form to use and what to re-check are in `docs/LAYOUT.md`
under "Rendering the grid (pending)".
Wanted but not started, recorded in `iris/TODO`: transforms on a move entry,
so a whole subtree scales or rotates with one buffer write and no redraw.
Compose-style stretch at the end of a scroll area is the use that prompted
it. A move entry only translates today, and composing through one scales the
`rel` part while `px` passes through untouched, so fixed-size content and
glyphs do not follow a shortened entry.
## Smaller layout items, none urgent
- Nested spans pass `leftover` weight up, so three leftover children in one
inner span beside one in another get three quarters to one quarter. No
other layout system does that; confirm it is wanted.
- A span can overflow itself without bound, so boxes of negative length reach
children and nothing states what a widget may assume about one.
- A `leftover` under a parent that does not divide is a minimum size --
`max(box, px + rel*box)` (Bryan, 2026-09-20), which is a `SizeRule::Min` of
`rel(1.0)` and shares `Painter::longer_than` with the span. Done in
`b295c8b`, and in `0d03267` for the root as well, which used to read it the
old way.
- `Fixed::div` by zero answers `MIN`/`MAX` while `ratio` answers `ZERO`; both
are caller bugs under `debug_assert`, but the fallbacks differ.
- `docs/LAYOUT.md` §4, §5 and the density section name `Painter::place`,
`Painter::region()`, `SetSize`, `desired_width`, `apply_rest`, `Len::dp`,
`Aligned`, which no longer exist; `MaxSize` now exists with the bounds API.
Do not restore `OnResize::Translate` or `OrthoSize`.
- **`layout/bounds` (`de1eb7e`) is included in #19** for the deferred
comparison implementation. A rule holds what a widget answers
(`SizeRule::{Min, Max, Clamp}`); a widget holds the box (`MaxSize`, which
`.max_width`/`.max_height` build). Bryan settled the split on 2026-09-20
after four readings were measured; the reasoning is in `docs/LAYOUT_LOG.md`.
Bryan superseded that split with the built-in bounds work above. The
deferred-request work fixes stale relative-bound reuse by tracking the
input base and comparing resolved bounds, with a reduced natural-size-hint
regression and 400 depth-5 relative-bound trees. Intrinsic bounds remain
`Len`s; comparisons involving leftover use `SizeRequest`. Ordinary generated
trees retain pixel bounds; `deferred_generated` supplies relative bounds and
expressions in separate corpora.
- `LazySpan`.
- `Scroll` taking a direction rather than one axis.
Other product work is in `docs/PLAN.md` and the focused documents it links.
Do not mix it into the Iris layout branch.