Files
ai-app/docs/HANDOFF.md
T

16 KiB

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.

A quality sweep of the deferred system is submitted on PR #19

layout/one-ask is at 4cb6f68 in /home/bob/repos/iris-deferred. The tenth sweep 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 expected 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 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. 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; every commit since has been byte-identical to 84dad21.

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.
  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_aligns 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. 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 Lens; 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.