Files
ai-app/docs/IRIS_EXTRACTION_HANDOFF.md
T

308 lines
16 KiB
Markdown

# Iris extraction handoff
Operational handoff for pulling Iris out of ai-app into a standalone framework.
Not a decisions log; delete it when the extraction is done.
## Where things stand
Canonical `main` is **`43ce8c7`** (#12, pointer routing). Fourteen slices are
in.
- **#16** `split/16-draw-size`, worktree `/home/bob/repos/iris-pr16`, head
`f192f75`. A widget sizes itself while drawing; `SizeCtx`, the `desired_*`
methods and the size cache are gone, and `size_dependence(axis)` drives the
retained path. `tabs`, `view` and `minimal` render byte-identical to `main`.
Check for a review before starting anything, and read the newest
`submitted_at` rather than the first result:
```sh
TOKEN=$(cat ~/.config/gitea/token)
N=12
curl -s -H "Authorization: token $TOKEN" \
https://git.arirex.me/api/v1/repos/iris/iris/pulls/$N/reviews
curl -s -H "Authorization: token $TOKEN" \
https://git.arirex.me/api/v1/repos/iris/iris/pulls/$N/reviews/<id>/comments
curl -s -H "Authorization: token $TOKEN" \
https://git.arirex.me/api/v1/repos/iris/iris/issues/$N/comments
```
My replies are ordinary issue comments on the same PR and say what each change
was for. `/home/bob/repos/ai-app-2` is on `rustify`, worktree clean.
## How the work is sequenced
**Most fundamental first**, from the owner on 2026-09-13: *"please do more
fundamental changes first, such as library updates and core framework changes,
so that code only has to be written once"*, and *"should probably start adding
tests early on rather than later, so you don't have to make separate test
scripts and stuff. This might involve making the harness eventually, depends
on what needs tested."*
So slices are ordered by how much depends on them, not by what is nearest
ready, and a slice arrives with tests rather than with a script in `/tmp`.
**Agree a design before sending another variation of it.** The owner stopped
the fourth round of #11 with *"we should probably agree on the design here
rather than you keep submitting variations that I review"*. When a review comes
back about the shape of something rather than a defect in it, put the options
and a recommendation in front of her and implement what she picks.
**Nothing is submitted without a separate review pass** — the installed
`pre-submit-review` skill: build clean, review the code, review the comments on
their own once the code has settled, then verify the claim by running it. The
fixes a review produces are themselves unreviewed code, so the passes repeat
until a round finds nothing. It has earned its place repeatedly: four defects
on #11 that format, clippy, tests and five headless renders had all passed, and
on #12 a regression introduced by the review's own first draft. `audit.sh` in
the skill directory prints every comment line a branch adds against a base ref;
the owner's standing complaint is verbose agent comments, and the default
verdict is delete. **Machine-specific notes do not belong in the repository**
they live in `~/.claude/MACHINE.md` or a `this-machine-*` skill.
Other standing instructions from the owner:
- Pull Iris out even if the Rust application switchover is not accepted. No
app, session, transcript, setup or server concepts in Iris; the dependency
runs one way from `app/` to Iris.
- **Small, coherent PRs.** The original extraction PR was too large to review.
A slice may be redone rather than transplanted, and need not remove every old
feature. Non-conflicting pull requests may be open at once — disjoint path
sets, each branched from current `upstream/main` rather than stacked. The
owner reviews small ones as they arrive and only avoids having two *large*
ones in flight, which is one more reason to keep a slice small.
- **Order by dependency, largest reach first.** On 2026-09-13: *"do the large
reaching framework changes first so less has to be redone."* Pick the next
slice by how much sits on top of it, not by what is nearest ready, so each
piece of code is written once against the framework that will exist.
- Do not recreate an `ai` branch in canonical Iris; the fork is the boundary.
- **Never rewrite a pushed branch.** Follow review with additive commits, and
merge `upstream/main` in rather than rebasing when a branch falls behind.
- Respond to each review finding with a fix or a concise explanation. Do not
add a ceremonial comment when the changed code already answers it.
- **A test has to guard something that could break again.** The owner deleted
#14's test as pointless: the rename it guarded cannot regress. When a fix is
structural, the structure is the test.
## The next slice
**Widget draw size and measurement** (source commit `6671194`), because it
reaches furthest: `desired_width`/`desired_height` and `SizeCtx` go, a widget
sizes itself during `draw` with `painter.set_size`, and `size_hint` is the
cheap answer for a parent that needs a size without drawing. Every widget's
body changes, so anything written before it is written twice. It also deletes
the separate measurement recursion and its cache.
It is testable now that `iris::harness` is in: `tests/layout.rs` is where the
acceptance goes.
Two things to settle with the owner before writing it. The archive's `Widget`
grows five methods — `size_hint`, `is_size_independent`, `requires_exact_region`,
`access_role` and `child_order`; only the first three are sizing, and she
dislikes capability methods on the core trait, so `requires_exact_region` in
particular wants justifying (`Pad` sets it and then clears it when the content
fits, which reads as a workaround). And the archive writes all of it against
`LayoutLen` and `density`, which canonical does not have — recreate it on
today's `Len` and let the dp slice follow, rather than pulling both in at once.
Still in the target, roughly in dependency order:
- **The position chain** (LAYOUT.md §2): `UiData::move_offsets`, per-primitive
slot ids and a bounded chain walk in WGSL, so moving a subtree writes one
slot instead of every descendant's primitives. Agreed to come directly after
#16.
- **Built-in alignment, and possibly size**, directly after the position
chain. A child of a span is handed the full extent on the ortho axis, so
`.width(rel(0.5))` inside a `Dir::DOWN` span changes what the child reports
and not the box it gets. **Do not "fix" this by reading the child's ortho
`size_hint`**: a `Pad` between the `SetSize` and the span has no hint of its
own, so the declared width silently goes back to filling. It works only when
nothing is in the way, which is the shape of bug the review pass is for.
Alignment has to belong to the widget rather than be discovered through
whatever happens to sit on top of it. Size is the harder half: a declared
size beside `set_size` is two sources of truth for one thing, so settle what
each means before building it.
- **`Len`, `LayoutLen` and dp.** The archive splits the type so that `rest` is
unrepresentable where it is meaningless (a padding), and folds a density in
at resolve time. 21 files mention `Len`, so it is wide but shallow. After the
draw-size slice, not before: that one deletes the `desired_*` bodies this
would otherwise have to be threaded through.
- **The input restructure** — `src/default/sense.rs` becomes `src/rsc/sense.rs`
(308 lines to 2313), plus `core/src/event/controller.rs`, `desktop/input.rs`,
`android/input.rs` and `sense_tests.rs`: pointer capture, drag slop and axis,
platform cancellation, mask-aware hit testing, event timestamps. The
archive's own `consumes` is what #12 landed, so that part transplants;
`tests/pointer_routing.rs` is the acceptance criterion.
- **The headless rig.** `scripts/run-headless.sh`, `headless.conf` and
`rig-input` are still only in ai-app's iris submodule, so a rendering claim
in canonical Iris is still verified by hand from another checkout. Small, and
independent of everything above.
- Retained span, scrolling and layout placement.
- Retained paints, selection, overlays and shared UI runtime state.
- Generic desktop/Android framework hosts and reusable example/APK tooling.
- Application-owned fonts and application-named font families.
- Shared resource-handle bookkeeping and replaceable glyph-atlas buckets.
- Positioned text overflow and cluster-safe ellipsis.
Dependencies are current apart from `winit`, which stays on 0.30.12 until
0.31 leaves prerelease. `parley` 0.11.1 and `image` 0.25.10 are latest.
```sh
cd /home/bob/repos/iris && git fetch upstream
git diff --stat upstream/main..origin/archive/full-extraction
```
The archive is a reference, not a patch to apply. Recreate a change on top of
canonical `main`, leave app-specific behaviour out, and verify it
independently. Pick disjoint path sets when two PRs are open, and branch each
from the latest `upstream/main` rather than stacking — unless the slice fixes
code another open branch replaces, in which case say so and stack deliberately.
## How the renderer works now
Current invariants, not history. Worth reading before touching `core/render`.
- **A primitive registers itself by being drawn.** The type carries its own
WGSL, and `PrimitiveRegistry` keys ids by `TypeId`, so the kind comes from
the type and there are no `RECT`/`GLYPH`/`TEXTURE` constants. Nothing is
seeded, so an id depends on what a ui drew first and a ui pays only for the
pipelines it uses.
- **Each primitive records its own draws.** `Primitive::render` makes a
`PrimitiveRender` that states the layout its shader reads, uploads whatever
it owns, and records its draws. `GlyphRender` owns the atlas and binds it
once per list; `ImageRender` owns the images and binds one per instance; the
default owns nothing and draws every instance in one call. The renderer sets
the pipeline, the shared group, the list's data and its vertex buffer, and
knows nothing else. Dispatch per list was measured at 6 instructions, 0.1% of
a frame at 256 and at 1024 layers, against the ~5,400 wgpu spends recording
one list; `tests/draw_cost.rs` is that measurement.
- **The shared bind group is the window and the masks**, given to every draw.
A mask texture would go here too. What a primitive samples is its own group,
and a primitive that samples nothing has no such group in its pipeline.
- **Every binding size is stated.** A `None` minimum puts the binding on
wgpu-core's late-sized list, which `is_ready` scans on every draw.
- **`shader/prelude.wgsl` plus one file per primitive**, because one module
cannot declare two types at the same binding. The prelude carries only what
every primitive uses — window, masks, vertex shader, `masked()` — and its
header is where binding numbers are written down; what a shader samples is
declared by that shader.
- **A texture handle is drawn like anything else.** `Painter::primitive` takes
`impl PrimitiveLike`: a primitive, or something that yields one and does
whatever else drawing it needs — a `&TextureHandle` retains its share on the
way through, which a `Pod` primitive cannot.
- **Order within a layer means nothing**, and the widgets do not rely on it:
`Stack` gives each child its own layer and `TextEdit` draws its view in a
child layer above the selection rectangles.
- **Images are one texture and one bind group each**, so each drawn image is a
draw call. The owner chose that on 2026-09-13 over packing images into
arrays like atlas pages; a bindless `binding_array` was ruled out by Android
support. Revisit only with her.
- **Layers are never freed** (`TODO` in `primitive/layer.rs`), so every layer a
session creates is walked every frame thereafter. Measured at ~2ns per empty
layer per frame, which is why it is the TODO's problem and not a bug of its
own.
## Repository topology
### ai-app checkout
- `/home/bob/repos/ai-app-2`, `origin = git@git.arirex.me:iris/ai-app.git`,
branch `rustify`.
- `iris/` is a submodule pinned at `32f6ad8`, the complete extracted snapshot,
and `.gitmodules` points at the **bot fork**, not canonical Iris.
- Do not change either casually: ai-app needs the complete snapshot while
canonical Iris is only partly caught up. Reconcile when canonical contains
what ai-app needs, or when the owner accepts a temporarily non-building pin.
### standalone Iris checkout
- `/home/bob/repos/iris`, `origin` = fork, `upstream` = canonical.
- Fork `main` and `origin/archive/full-extraction` both name `32f6ad8`, the
target snapshot. `history/full` names the source-history result `a615bcd`.
- **Do not reset, overwrite or force-push fork `main`**: it is both the target
reference and the commit ai-app pins.
- Start each new branch from current `upstream/main` in its own worktree:
```sh
cd /home/bob/repos/iris && git fetch upstream
git worktree add -b split/15-name /home/bob/repos/iris-pr15 upstream/main
```
Every other `/home/bob/repos/iris-pr*` worktree holds a merged branch. They
are readable references; do not build new work on them.
## Verifying a slice
```sh
cd <iris-worktree>
cargo fmt --all --check
cargo clippy --all-targets -- -D warnings
cargo test
```
`--workspace` when the slice crosses workspace crates. A rendering claim needs
a real run, which until the rig is extracted means driving it from ai-app's
submodule:
```sh
cd /home/bob/repos/ai-app-2/iris
./scripts/run-headless.sh tabs --dir /home/bob/repos/iris-prNN \
--shot /tmp/out.png --seconds 3
./scripts/run-headless.sh tabs --dir ... --replay /tmp/taps.touch --shot ...
```
A `.touch` file is `<ms> down|move|up <x> <y>` in the output's own pixels. The
`tabs` example's five tabs sit at x = 192, 576, 960, 1344 and 1728 on a
1920x1200 output; tapping the third and then the bottom-right button twice
adds two images.
Three renders cover the drawing paths, and each needs a different ui, so two
of them are throwaway examples written into the worktree and deleted after:
1. `tabs` with that replay — rects, glyphs and images together.
2. An image alone in a layer, which is the case that failed GPU validation
when every other test happened to have a rectangle in the same layer.
3. Six lines of 400px text, which forces the atlas to four pages and proves
the array grew and its group was rebuilt.
Those three become ordinary tests with the harness slice, which is the
argument for doing it next.
## Cautions
- Read `/home/bob/repos/ai-app-2/AGENTS.md` and the machine-wide rules first.
Anything about this machine — the GPU that comes and goes, measuring a small
performance difference, the emulator — is in `~/.claude/MACHINE.md` and the
`this-machine-*` skills, and belongs there rather than here.
- Keep Iris generic: session drivers, transcripts, setup and server concepts,
app icons and product fonts stay in ai-app. Android and desktop code is Iris
work only when it is a generic host or platform integration.
- Preserve the dirty-worktree rule. All worktrees were clean at handoff;
anything found later may be the owner's or another agent's.
- Do not delete the archived snapshot or the fork `main` ai-app pins.
- A complete target branch is not permission to recreate the giant PR.
- Another agent was freeing disk on this VM and removed `target/` from the
`iris-pr*` worktrees once. Sources and git state were untouched. Tell peers
before changing shared machine tooling, and expect a cold rebuild sometimes.
## Merged so far
| PR | On canonical `main` |
| --- | --- |
| #2 | Build on the current nightly (`4275314`) |
| #3 | Request a frame after resize (`936fbdd`) |
| #4 | Decouple `iris-core` from winit (`465e430`) |
| #5 | Use vsync by default (`ec2b5d4`) |
| #6 | Notify winit before presenting (`db9b0f2`) |
| #7 | Keep unsafe reference helpers internal (`0191f20`) |
| #8 | Initialize the window uniform from the surface (`6e271e8`) |
| #9 | Preserve primitive-count recursion (`b90c855`) |
| #10 | Text layout and rendering on Parley (`0f6a28b`) |
| #11 | Atlas as an array texture, and the primitive rendering overhaul (`b234497`) |
| #13 | Build on wgpu 30 (`00d2230`) |
| #14 | Rename the `Sized` widget to `SetSize` (`32b1038`) |
| #15 | Run a ui without a window, and test one (`c8ac669`) |
| #12 | Route pointer input per kind (`43ce8c7`) |
URLs are `https://git.arirex.me/iris/iris/pulls/{number}`.