# TODO

Work on this framework that is known about and not done, organised by [what v1 means](v1.md): **an
application built on v1 does not need a major refactor, and later releases add to the abstractions v1
already has.** So the first question about an entry is not how big it is but whether landing it *after* v1
would break an application that compiled before. Most of it is what the ports turned up — what
[the text editor found](architecture.md#what-porting-the-text-editor-found), and what
[the designer found](architecture.md#what-porting-the-designer-found) — where the gaps each port
closed, and the reasoning behind each, are recorded — and, since October, what the applications generated from the
template wrote in their own `docs/framework-notes.md`. Since 2026-10-22 each milestone of an application ends in a
[retrospective](developers/retrospective.md) in its `docs/retrospective.md`, and its numbered proposals come here,
under *Found by \<application\>*, each linking back to the section it came from.

Keep an entry short enough that it does not need editing, and delete it when it is done rather than
ticking it. An entry whose fix belongs in a sibling repo says which one; the ones under **Upstream**
cannot be fixed from here at all. An entry moves between sections when the answer to *would landing it
later break an app?* changes, and says why.

**A ruling carries its reason.** Anything recorded as ruled here, in [components.md](components.md), in
[threading.md](threading.md) or in [v1.md](v1.md) says *why* in the same place: the constraint it comes from, the
evidence that decided it, or the cost it knowingly accepted. A ruling with no stated reason is an idea that sounded
good at the time, and a later reader cannot tell the two apart, so it has not been ruled yet. When the reason stops
being true the ruling is re-argued, not kept for being old. A ruling that turns out to have been wrong is corrected
in place and says what corrected it, as the capture rule's entry does for components.md.

Two labels keep the difference visible. **Ruled** means the user weighed it and gave, or accepted, a reason.
**Accepted as recommended** means a recommendation was adopted without discussion: it is sound as far as its stated
reason goes, and has not been examined beyond that. Neither is permanent, and the second is the one to re-check
first when something depends on it.

## Blocks v1

Landing any of these after v1 would break an application that compiled before, or change what an
application already depends on. Ordered as [v1.md](v1.md#how-to-get-there) orders the work.

- [ ] **Geometry is written in `Length` and read back in four kinds of px** (mostly upstream, `vexelray-gui`).
      Audited in [units.md](units.md) (2026-10-05), from the text editor's FN-20. There is no public em basis,
      so eleven sites convert by hand and three of them are wrong (`SplitPane.size`, `SplitPane`'s drag
      limits, `Tooltip`'s gap). Every one is hidden while `dpi` is pinned to 1, and surfaces when E4 lands.
      **Why this blocks v1:** `SplitPane.sizeDp`/`onResize(Consumer<Float>)`, `Table.columnWidth` and the
      `NodeLayout` record's components are public. **U6 is ruled and built (2026-10-15):** em is the node's own
      text size and rem the root, each caller of the old em having moved to rem first, so nothing moved. The rest
      of the recommendation (U1–U5, U7–U9) is **not yet ruled**.

- [ ] **The container now gives a component a thread and a mailbox; what is left is the colour rule.**
      `Shell.lanes()` owns the application's threads and the generated wiring puts each `@Component` on one of
      them with its mailboxes, its wake and its drain-then-stop — `Lanes` in `-core` (pure JDK, so the
      container stays testable with no GPU) and `Placement` in `-shell` (a mailbox is atchung's, and that
      edge already falls there). The upstream half landed with it: `Gui` takes both lanes rather than
      building a `newCachedThreadPool` whatever it is handed, and closes only the lanes it built itself.

      **Why this blocks v1:** a rule that rejects code v1 accepted is a breaking change, so the copier and capture halves, the message-graph checks and supervision land before v1 or behind an opt-in. They also decide what a component owes the rest of the application, which is how every component is written.

      **Nineteen of [threading.md](threading.md)'s rules are now held.**  Eight came from these two objects, and
      none of those promotions needed the processor — they needed something to *be* the rule; four remain the
      processor's (2026-10-02). The `upstream` column is
      down to one entry, because T1.2 and T1.5 turned out to be the same change.

      **What is actually left**, and it is the part a runtime object cannot hold: the colour rule's copier
      and capture halves (T2.4, T2.5 — T2.1–T2.3 are the processor's now, and held), the message-graph
      checks (§4.1, §4.3–§4.5), and the rest of supervision (§6.3). A lane that has stopped draining is now
      noticed and named (`Watchdog` and `Lanes.busy`, see *Build the liveness guarantee*), but on a timeout only:
      `Overrun` surfaced per grouping is still the better signal. Plus the handler lane's bound, which waits on a
      census of what still blocks on a handler rather than on any one known blocker.

      **Ruled 2026-09-28: the component model — multithreaded by default — is a design seam frozen at v1.** Groups, lanes, channels, loss
      classes and supervision are not deferred behind the version number: v1 ships them declared, checked and
      frozen, or it does not ship. That makes the declaration side a v1 build item and not a later feature, and
      it makes the eight decisions in the threading audit (the imperative path, public `Shell.place`, `lane`,
      group = lane, the `@Subscribe` defaults, supervision, the landing rule for checks, the tree) rulings owed
      before the seam is built. There is no single document for it yet; see the overview entry below.

      **Ruled: there is a default component lane, and it is not the GUI main thread.** A component that
      declares no `lane` runs on it, so all such components share one thread by default; `lane` is an optional
      override, and two components naming the same lane share that one instead. The main thread stays a
      separate colour (`@MainThread`, never a lane), and the default lane is a lane of its own beside the
      handler and offload lanes in `Lanes`. The default is the contract, because moving it later changes timing
      and blocking behaviour without a compile error. This is what makes *multithreaded by default* cheap: the
      thread count does not grow with the component count, and a plain service costs nothing to make a
      component. **What it costs:** one slow component holds up every other on the
      default lane, so isolating one is `lane = "..."`, and supervision has to name a *lane* that has stopped
      draining, not only a component.

      **Built:** `@Component`'s `lane` is optional and defaults to `Component.DEFAULT_LANE` (`"<default>"`),
      which is one shared placement, so one thread. It is reserved by construction: the processor requires an
      explicit lane to be a name (`[A-Za-z][A-Za-z0-9._-]*`), which the default is not. Nothing to bound: the
      lane is one thread and each mailbox already has a capacity. Default-lane components may hold each other
      and not a named-lane one (T2.3).

      **Ruled: the default lane is under supervision.** The framework watches it always on, as `Stalls` does
      the main thread, and a lane that has stopped draining is reported by name. That puts the per-lane
      liveness signal in `Lanes` and `Placement` on the v1 build list, at least for the default lane, and it
      is the case that made the observer more than optional: one slow component holds up every other on it.
      **What supervision does is still only report**, and that is frozen: a component's failure never stops
      the others, a stalled lane is named and not killed by the framework on its own, and richer policies are
      additive (superseded below: the default action is now not contractual). **Corrected 2026-10-01:**
      *a component's failure never stops the others* is true of a failure and false of falling behind, because a
      full FAIL mailbox halts the process on purpose ([components.md](components.md), ruling 5). **Corrected again
      2026-10-18, and now false of a failure too:** ruled that every failure is either declared and handled or ends
      the application, so a handler that throws halts through the fault policy rather than being logged and drained
      past (components.md, section 10). What stays true is that a *stall* is named and not killed. **Open:** the stall
      threshold and where it is set, whether lanes an application names are supervised the same way (the
      mechanism is per-lane, so probably), and what a report looks like to an application that wants to react.

      **Ruled: the liveness guarantee** ([v1.md](v1.md#the-liveness-guarantee)). A wedged component never
      stops the window responding, the user always being able to close, or the application ending itself. That
      turns three things into v1 items: main-thread code may not send on a blocking channel (a processor check,
      and `Backpressure.BLOCK` from the main thread is the freeze the upstream-topics entry already found),
      close has a total bound after which the process halts, and the watchdog is a thread on no lane with an
      explicit `halt`. **To confirm:** the bound (a number and where it is set), and whether a user's close
      halting the process after it is a default action. It is user-directed rather than autonomous, so
      recommended yes. **Test it in W2:** a *wedge* button, then assert the window still repaints and takes
      input, and that close exits within the bound.

      **Ruled: what a wedge does is a policy the application can override, and the default is not part of the
      contract.** The frozen parts are the liveness guarantee and the seam that takes the policy, a provider
      handed back like the look and the input backend, told what happened and acting through a context object
      (`exit` now, `restart` later), never returning an enum. The default today is report and then exit,
      matching the bus-fault precedent. The target is a main-thread fallback offering the user a restart.
      **Realism, unverified:** a fallback on the main thread works for a wedged *lane*, because the default
      lane is not the main thread, and cannot work for a wedged main thread, where the way out is halt.
      *Restart* comes in two sizes. Relaunching the process (start a new one from the same command, then halt)
      is small and needs only what the app already persists. Restarting components in place is not: Java
      cannot stop a wedged thread, so the old one would be abandoned and could wake later on stale state, and
      the container builds each part once, so it would need a rebuildable subtree and the tree's depth-first
      teardown (T6.3), which is unbuilt. Do the first as the target and leave the second to an opt-in policy.
      A default of *exit* on a stall also throws away unsaved work, so the threshold has to be long and the
      policy needs a bounded chance to save first.

- [ ] **The component model's nine rulings were accepted on 2026-10-01 (mostly as recommended), and part of what they ordered is not built.**
      [components.md](components.md) records them. The imperative path is removed (rulings 1 and 2, done; T1.3 is held). The tree spike is done (ruling 8: zero existing signatures change).
      The copier's check half is done (2026-10-02: a `@Subscribe` payload must be deeply immutable; the generated
      copier is parked as additive). Open from them: the capture rule, which needs a decision (see its entry below).


- [ ] **One seam carries three of the four differentiators, and it is the one not built.** A
      `@Subscribe` that generates its `Pump` registration in `ATTACH` and its teardown in the
      `Disposer` is at once the actor model's substrate (above), the extension and macro API, and the
      reactive half of automation. That third one is easy to miss: `Automation` is command-in,
      response-out — `click`, `type`, `shot`, `await` — so a user-authored macro can *drive* the
      application but cannot *react* to it, and an extension that wants to run when something happens
      has nowhere to attach. All three want the same thing, which is why it is worth building once
      rather than three times.

      **Why this blocks v1:** the [extension test](v1.md#the-extension-test) fails on it twice: elektro-Q and reactive automation have nowhere to attach without it. It is also where a component declares its channels, so it reshapes component code written before it exists.

      **First slice built (component mailboxes).** `@Subscribe(topic, overflow = FAIL, capacity = 64)` on a
      `@Component` method registers a mailbox on the component's lane where the wiring constructs it, with the
      payload type taken from the method's one parameter; the placement's close is the teardown, so there is
      nothing to generate for it. `@Publishes` declares what a type or method sends on. Held by them: T4.1 (an
      edge and a sample on one topic, or one topic name with two payload types), T4.3 (a mailbox per method),
      and T4.7 (main-thread code declaring a send on a `BLOCK` topic). **Not built:** the rest of the seam. A
      `@Subscribe` that is not on a component (an extension or a macro, which is the whole point of the entry),
      registration in `ATTACH` and teardown through the `Disposer` for something that is not a placement, and `Fold`
      (the blocking-cycle check, T4.4, is built, and the graph is exposed as generated data, T4.5, 2026-10-19: the
      wiring's `BUS_GRAPH`, for the Inspector's bus circuit). The three
      `@Subscribe` defaults (FAIL, 64, registered at construction and started with the lane) were
      accepted as recommended on 2026-10-01 and are frozen, capacity included ([components.md](components.md),
      ruling 5), because changing 64 later changes whether an application dies under a burst.

- [ ] **Nothing in the model covers work that outlasts a frame, and the stack already named the
      answer.** `kronometer/docs/architecture.md` §10 specifies it: *"`offload(work)` remains available
      for work that is genuinely unbounded — file I/O, network, image decode — moving it to an ordinary
      executor (**its own**, never the kernel's single carrier) and delivering completion as a timeline
      event."* Specified and unbuilt — `offload` appears in no Java source in that repo. The framework
      wants the same lane, and naming it here is what stops the component model being read as the answer
      to a question it does not answer.

      **Why this blocks v1:** the completion path is what application code will call to get a result back onto the frame loop. The pool is the easy half; the way back through the timeline is the API.

      **A pool does not contradict static placement.** A component is placed statically because it is
      stateful and ordered; an offloaded task is stateless and unordered, so there is nothing to confine
      and no sequence to keep. The edge is policed by [the crossing
      rule](architecture.md#what-may-cross-a-lane-and-what-crossing-does-to-it) — a value crossing a lane
      arrives as if it had crossed a wire — which makes an offloaded lambda that captures a component's
      state a compile error rather than a race. Capture is the one case the rule refuses rather than
      copies, because a lambda closes over a reference and no copier can be slipped in behind it.

      **Platform threads, and the reason is not the component one.** Blocking I/O is the textbook
      virtual-thread case, but §3.1 records that `jdk.virtualThreadScheduler.parallelism` is a global JVM
      property with *"no public per-thread scheduler"* in JDK 25 — so an application that takes
      Kronometer's 3× baton flag has no second carrier to give this pool, and the obvious choice is the
      documented deadlock again. That flag is the application's and correctness never depends on it, so
      the framework's default has to be the one that is right when it *is* set.

      **The completion path is the content; the pool is the boring half.** A result is published on a
      `Topic` and folded into a `Cell` by `KronBridge`, or dropped on a queue drained in
      `FrameStage.APP` — the two doors that already exist, and `APP`'s own list is *"a history to
      restore, a file to open, a preview to render."* What an offload thread must never do is touch the
      tree or the timeline in place.

      **The lane now exists.** `Lanes.offload()` is bounded, platform, and separate from the handler lane;
      `Gui.offload()` is the same lane reached from a widget. Two lanes rather than one, mirroring the split
      Kronometer already makes between the precompute pool and `offload`.

      **What this entry got wrong is worth keeping.** It said `FileActions` called `Files.write` and a
      blocking `FileDialog.save` *"inline on a handler thread"*, sharing an unbounded lane with click
      dispatch. It did not: every command there goes through `GuiApp.post`, so the I/O was on the **GUI
      thread** — which is the right place for the dialog, whose contract requires it, and a worse place for
      the blocking call than the handler lane would have been. A read from a mount that had gone away held
      the frame loop rather than one document. Both are now on the offload lane and land back through
      `app.post`; the dialogs stay where they were. **The lesson is about the inventory, not the editor:** a
      claim about which thread something runs on was written once, was true once, and was not re-checked
      when `app.post` moved it.

      **Still to do**: `kronometer`'s own `offload` remains specified and unbuilt, and the framework does not
      yet route completions into the timeline — the second door (a `Topic` folded into a `Cell` by
      `KronBridge`) is unused, and everything that lands today takes the first one.

      **Found 2026-10-02, reading what `work` may do** (the callback that runs on the offload lane in
      `GuiApp.offload(work, landed, failed)`). Gaps in what is checked (an `Error` from `work` used to reach neither callback; since 2026-10-18 it
      reaches `failed`, wrapped in an `ExecutionException`, and a `VirtualMachineError` is rethrown after):
      - **A send from `work` is invisible to T4.4.** `@Publishes` goes on a type or a method and a lambda has no
        declaration, so the cycle check cannot see that `work` blocks sending to a mailbox whose owner is waiting on
        this very job. A blocking cycle through an offloaded task is a deadlock nothing reports.
      - **The result `T` crosses a lane and is not run through T2.4**, and `landed` and `failed` always return to
        the main thread rather than to the caller's own lane, so a callback written on the handler lane that
        captures handler state races. Delivering completion to the owner's lane, or as a message to a component's
        own `@Subscribe`, would remove the capture question for those two. Only `work` would be left with it.
      - **Cancellation and interrupt do not reach it** (already listed under *Build the liveness guarantee*): the
        offload pool is not in a `ThreadGroup`.
      - **Unverified:** whether `work` mutating a `Node` directly is legal. `Gui.async`'s Javadoc says a result
        can come back "through a node mutation"; this entry says `work` must never touch the tree in place.

- [ ] **The wiring is generated; what it does not do yet.** The processor writes `<App>Wiring` from
      `@VexelApp`, `@Provides`, `@Component`, `@Setting`, `@BeforeFrame`, `@OnMode` and `@ConditionalOnType`,
      the `vexel-desktop` template ships a `Recipes` configuration instead of a hand-written wiring, and
      `-Pacceptance` builds and drives the result. The **resolution rules are the contract** — an application depends on which provider satisfies a parameter without ever writing it — so the decisions below are v1 items and the additions are not. Each bullet says which:
      - **Additive, once the rule is written.** **Most of the framework's defaults still cannot be replaced.** The look, the input backend and the
        clipboard can — a `@Provides` returning one is handed back to `Shell` (see architecture.md, *the
        framework's own defaults are handed back*). So can the icon, which is named rather than provided:
        `@VexelApp(icon)`, falling back to the framework's mark (architecture.md, *the application's mark*).
        Window memory, the dialogs and pacing cannot, and the README lists them among the defaults. Each wants a
        reason to be replaced before it gets a setter; none has one on the stack yet.
      - **Additive, but before a BOM.** **A starter is not checked where it is compiled.** `AutomationStarterTest` compiles applications
        against `AutomationStarter` with the real processor, which covers the one starter there is; the
        processor is not on `-automation`'s own `annotationProcessorPaths`, so a starter's library-level checks
        run only when an application names it. Worth wiring when there is a second starter, and before a BOM.
      - **Ruled and built 2026-10-18: a provider returning `null` is passed on and reported once.** The generated
        wiring calls `Absences` (`-shell`), one WARN on `framework.wiring` naming the provider and each part built
        with the null (named in atchung's logging.md). **Why:** `@Provides` already promised it, an application returning null on purpose keeps
        working, and a startup error would have needed an "optional" annotation to say which nulls are meant.
      - **Ruled 2026-10-18: types match exactly.** A parameter asking for an interface is resolved only by a provider
        returning that interface, never by one returning a subtype. **Why:** the two directions are not symmetric.
        Allowing a subtype later, and only where there is no exact match, turns errors into successes and changes
        nothing that compiled; allowing it now means an application that adds a second subtype of something a
        parameter asks for stops compiling on an ambiguity it never wrote. Exact is the one that keeps the door
        open.
      - **Additive.** **One round.** The graph is taken in the round the `@VexelApp` is seen, which is every source of a
        clean build; anything another processor generates in a later round is not in it.
      - **Decide now.** **Two promises need a method body.** A `@Provides` calling another directly, and T2.5's capture
        rule, are both about what code *does* rather than what it declares. The Trees API can read them;
        doing so is a decision, since *the processor reads declarations* is load-bearing in
        architecture.md's argument about placement.
        **Found 2026-10-02 while scoping T2.5, and it corrects components.md:** capture cannot be made impossible by
        construction. Offload is an `Executor` (`Lanes.offload()`, `Gui.offload()`) and `GuiApp.offload(Callable, Consumer,
        Consumer)`, and Java has no type that refuses a lambda that closes over something. So the choices are a check
        that reads the lambda through the Trees API with attribution, from a javac task listener (accurate: it resolves
        each name to a local, a field or `this`, and runs `Shareable` on what it finds; and the heaviest piece in the
        processor, which has never run outside the annotation rounds), or leaving T2.5 as a rule the reader keeps. A
        syntactic check without attribution is not an option: it cannot tell a captured local from a class name.
        **Not decided; a third option is proposed** ([components.md](components.md), section 10, 2026-10-18): slow
        work as a declared `@Job` whose result is published on a reply topic, so there is no lambda to capture with
        and the check reads declarations only. To rule on: adopt it, and failure's shape.
      - **Before v1, with the component model.** **Done 2026-10-02:** the copier's check half (a `@Subscribe` payload must be deeply immutable) and the blocking-cycle
        check (T4.4). **Additive, not built:** the generated copier. **Done 2026-10-19:** the graph exposed as data (the wiring's
        `BUS_GRAPH`, vexelray-inspector `docs/bus-circuit.md`).

- [ ] **`vexelray-engine` is a second composition root, and `GuiApp` is the first.** The new module (in
      `../vexelray`) exists because *"six demos each carry a copy of eighty lines of
      instance/device/swapchain/presenter wiring"* — and `GuiApp` is a seventh, doing `new
      VulkanInstance`, `selectGraphicsPresentDevice`, `new VulkanDevice` and driving `WindowedPresenter`
      itself. Nothing is broken by that refactor: it kept every prior constructor and `Config` form
      deliberately, so the stack still compiles and this repo's tests pass against it. But
      `VexelEngine.create` resolves an `EngineProvider` through `ServiceLoader`, so if `GuiApp` is ever
      rebuilt on it the stack acquires reachability metadata an application inherits and this framework
      does not yet aggregate — the open question architecture.md already calls *"probably the
      highest-leverage feature not yet listed."* Nothing to do here until that seam moves; recorded so
      it is not a surprise when it does.

      **Why this blocks v1:** which root owns device creation decides what reachability metadata an application inherits, and so the build shape. Nothing to write until that seam moves, but the decision precedes the BOM.

- [ ] **`Gui`'s topics are `static`, so one bus can carry one tree** (`vexelray-gui-core`). Every
      instance subscribes to the same `vexelray.gui.mutations`, so two trees on one bus each receive the
      other's mutations — into a mailbox bounded at 65,536 with `Backpressure.BLOCK`, drained only while
      that tree is being presented. The second `Gui` fills and then blocks the first one's node setters
      for good: a freeze after tens of thousands of edits, with nothing thrown and nothing logged. This
      framework shipped it for three commits by putting `Modals` on `Shell.bus()`, and the designer found
      it by putting a second window there and watching the viewport stop marching.

      **Why this blocks v1:** it caps one fabric per application, so `Shell.bus()` cannot promise that a second window joins it. Either the fix lands upstream first, or v1 documents the ceiling as part of the contract.

      **A third time, 2026-10-07:** `Popout` did `new Gui(host.bus())`, the calculator's settings window
      copied that line, and the main window came up drawing the *settings* tree from its first frame, with
      nothing thrown (calculator FN-2). `Popout` now gives the popped-out tree a bus of its own
      (`vexelray-gui` `a93a661`). Each new second window was another chance to copy the wrong line, so since
      2026-10-11 the framework builds a named window's tree itself (`Windows.named`, on a bus of its own and the
      application's lanes), and no application on the stack makes a `Gui` for a second window any more.

      Constrained rather than fixed: `Gui(Atchung)`'s javadoc now says which things may share a bus.
      **It is also the ceiling on one inspectable fabric per application**, which is the point of having
      a bus at all — a second window cannot join it. The fix is topics named per instance rather than
      per class, and it is a real change in `vexelray-gui` rather than a line here.

- [ ] **What `Shell` hands over is the seventh surface, and it bounds the other six.** `Shell` returns
      `Gui`, `GuiApp`, `KronoGui`, `TitleBar`, `Settings` and `Atchung`, and since October `ComputeQueue`
      (`Shell.computeQueue`) and `dev.vexelray.os.Icon` (`Shell.windowMark`); `Windows`, `Window` and `Dialogs`
      are the framework's own and hand over `Gui`, `TitleBar` and `Modal` in turn (`WindowMemory` and
      `CloseRequest` are no longer handed over at all, since 2026-10-11). So an application holds
      `vexelray-gui`'s and `atchung`'s types directly and v1 is only as
      stable as they are. Either those repos freeze what is exposed, or `Shell` stops exposing it — a
      wrapper per type, or fewer accessors. A decision rather than a task, and it wants making per
      accessor rather than once. Every side of the edge is the author's own, which makes freezing cheap
      and makes not deciding the only expensive choice.

      **Ruled 2026-09-28: `Gui` is frozen** — the subset applications use, with the remainder marked
      experimental. `vexelray-gui/docs/plans/gui-decomposition.md` reports all seven decomposition steps done, and
      the last app-facing removal from `Gui` was `onCaretHit`/`onCaretDrag` on 2026-08-14; nothing local calls
      them. Still to do: classify the ~100 methods, and give `vexelray-gui` a marker of its own, since the
      framework's `@Stability` sits above it in the dependency order.

      **Ruled 2026-09-28: hide or delete what nothing needs; delete is preferred.** The `Shell.dialogs()` of the
      time was deleted: no caller, and it was not an injectable root anyone had a use for (the name came back on
      2026-10-11 for something else, the `Dialogs` root). Open then: `memory()` was an injectable root in the
      processor's table, and the text editor port had needed `WindowMemory` injected (architecture.md, *what
      porting the text editor found*), so *no caller* was partly the deleted witness. `app()` could not simply go:
      `Driver`, in a separate module, reached the window's `WindowControls` through it, so the choice was a
      narrower accessor for exactly that, or an explicit host seam.

      **Done 2026-10-11, on that ruling.** `memory()` is deleted, accessor and root: the windows that used it (the
      calculator's settings, W2's tool window) are remembered through `Windows` now, and a census of every
      application on the stack found no other caller. `Driver` reaches the windows through `Windows.views()`, the
      narrower accessor, and not the `GuiApp`.

      **Ruled 2026-10-11: `app()` stays, as the device and not the windows.** Reason: the same census found every
      remaining use of `GuiApp` is the device or a dialog — Pix's and Vexplore's textures, the fluid and rigid
      simulators' viewports and compute, and the dialogs of the text editor, Vexplore and Pix — and none is window
      management, which is all `Windows` now covers. The device is the main thread's by rule, so a recipe taking it
      is `@MainThread` correctly, and a narrower type for it would be the same boundary under another name. The
      dialogs are the `Window` seam entry's own bullet and move off it there. They have (`Dialogs`, the same day),
      and so has the one window-management use the census missed (Pix's viewer closing the main window, now
      `windows.main().close()`), so what is left behind the `GuiApp` is the device and nothing else: textures,
      viewports and compute.

- [ ] **Witnesses: the abstractions have been drawn from one application shape.** The three ported
      applications were deleted on purpose, which was right, and for a while left one generated counter-style
      application validating everything. (Since then the template writes a text editor, and Vex, the calculator,
      Vexplore, Pix and the simulation demos are generated from it; their findings are below. Most are
      one-window-plus-dialogs desktop applications; the calculator's settings and Vexplore's viewer are each one
      second window, and W1 is the one with several.) *One component is not a census* applies to the framework itself.
      Wants two or three generated applications that differ where it matters — several windows on one
      device, component-heavy with real placement, long-running with nothing to settle — each expected to
      find something, and each finding written up in architecture.md as the ports were. After the
      component model, since that is what they most need to exercise.

      **W2 (component-heavy) and W3 (long-running) are built** and pass: [what W2
      found](architecture.md#what-the-component-witness-found) is mostly confirmation plus two observations, and
      [what W3 found](architecture.md#what-the-long-running-witness-found) is two real findings, both filed below.
      **W1 (several windows on one device) is built, 2026-10-11, and passes**: [what W1
      found](architecture.md#what-the-two-window-witness-found) is what shaped the `Window` seam — named windows,
      the main window, the dialogs and the close gates — and found an upstream hang that is now fixed (the lost wake
      under *Upstream*). So all three are built; what is left is depth, not breadth: W2 lacks a wedge that ignores
      interrupts, which is what would make the halt backstop fire, and W3 is twenty seconds long, which rules out
      growth per tick and nothing slower.

- [ ] **Starters, a BOM, and the stack's reachability metadata.** The build shape is a contract: an
      application's dependency block and its `@VexelApp` are the first things it writes. One starter
      exists (`AutomationStarter`), so the abstraction is drawn from a single instance; a second starter
      is the test of it. The BOM and the aggregated native-image metadata are what make one dependency
      enough, and the metadata is what architecture.md calls *"probably the highest-leverage feature not
      yet listed."* After the component model, because what an application inherits depends on what the
      stack turns out to reach.

      **Ruled 2026-09-28: v1 may ship native on Windows only, provided adding another OS later needs no
      change to an existing application.** That makes where the metadata lives a v1 decision. It must be
      carried by each backend's jar (GraalVM merges `META-INF/native-image` from every jar on the classpath)
      or by a starter, never by a file in the application, or every new OS is an edit to every app. The
      per-OS backend selection in a starter has to meet the same test. **Corrected 2026-10-05:** this said no
      generated application had ever been built as a native image. The text editor, vexplore and mainframe now
      all are (Windows, GraalVM 25, `-Pnative` in each app), which is exactly the case the rule above warns
      about: each carries its own metadata and flags, see the next entry. `-acceptance` itself still stops at a
      JVM build.
      **Refined 2026-09-29: Windows is the v1 requirement; macOS is wanted but not required; Linux is in scope
      and will become load-bearing, but is not a priority until a distro is chosen and gives it a real build target.** The additivity test above applies to all of them, so what
      v1 must get right is where the metadata lives and how a backend is selected, not the number of backends.

      **Ruled: every native extension is cross-platform in theory, or wrapped in a facade for the future.**
      Platform-specific code never appears in a public signature or in an application's build. The stack already
      has the shape (`NativePlatform` behind a `ServiceLoader`, `InputBackend`, `ClipboardBackend`), so the rule
      is that a new native piece follows it and that an existing one which does not is a v1 fix. **To audit,
      unverified:** which native pieces have no facade today. The reachability report names FFM bindings in
      `vexelray-os-windows`, `tactroller-clipboard` and `tactroller-linux`, a native-file-dialog loader that
      extracts a library from a jar, and bundled `natives/` for image decoding.

- [ ] **Three applications carry their own native-image metadata and flags; it belongs upstream.** Found
      2026-10-05, building `text-editor-vexel-demo`, `vexplore` and `mainframe` as native executables (about
      30 to 40 s each, one 27 to 43 MB `.exe`, and each ran). Every one needed the same things written into
      the *application*: a `reachability-metadata.json` recorded by running the app on a JVM under the tracing
      agent, `-H:+ForeignAPISupport`, `--enable-native-access=ALL-UNNAMED`, and (mainframe, for
      `Arena.ofShared`) `-H:+SharedArenaSupport`. The first build of two of them failed at runtime, not at
      build time, on a missing FFM descriptor (`MissingForeignRegistrationError` in `User32.<clinit>`) or on
      `Arena.ofShared`, so what is missing shows only when the path runs. What the agents say belongs where:
      - **`vexelray-os-windows`:** the `Win32Window.wndProc` and `WindowsPlatform` reflection, the FFM downcall
        and upcall descriptors it binds, and the `NativePlatform` `META-INF/services` entry.
      - **`tactroller-windows`:** `WindowsInputBackend` and `WindowsRawInputHub` reflection, the `InputBackend`
        service entry, their descriptors. **`tactroller-clipboard`:** its descriptors.
      - **`vexelray-gui-nfd`:** the `natives/windows-x64/nfd.dll` resource and its downcalls. Whether its
        loader, which extracts a library from a jar, works in a native image is **not known**: the dialogs were
        never opened (`ottermate` cannot click a native dialog).
      - **The jars that own them:** the shader `.spv` and text-atlas resources (`vexelray-gui-draw`,
        `vexelray-text`, `supirvast`), and the joni `tables/*.bin`, TM4E reflection and signed-jar workaround
        (a text-highlighting module or the editor's own starter, not each app).
      - **A starter or the BOM:** the `-H` flags and `--enable-native-access`, the `native` profile itself, and
        the generic `java.awt`, `java2d`, ImageIO, `sun.security.provider.*` and `awt_en` entries.
      - **Already duplicated:** `vexelray-gui-demo` carries nearly identical metadata. Consolidate it into
        the same place rather than a fourth copy.
      **Why this blocks v1:** it is the ruling in the entry above, now with evidence: the metadata is carried by
      each backend's jar or a starter, never a file in the application, or every new OS or backend is an edit to
      every app. Today it is the other way round. **Unverified, for whoever moves it:** the metadata was
      traced from limited runs, so a path nobody exercised (the dialogs, the clipboard, mainframe's other
      shells) may still fail at runtime, and tracing is per application, so upstream metadata wants a trace of
      each backend's own test or demo rather than of an app.
      **Since 2026-10-09 the template writes the native build too** (`-Pnative`, `-Pnative-release`, the two
      editions, the icon, and a `reachability-metadata.json` holding only the entry point and the JDK entries the
      calculator's trace saw). That is the arrangement this entry says is wrong, now copied into every new
      project. It is the honest default until the profiles and flags have an upstream home, and it is small,
      because the libraries already carry their own metadata. When they move, the template's pom is the place
      to delete them from, with the five applications.

- [ ] **The `Window` seam: built, and one choice left.** `memory()`, `app()` and one-off dialogs were each a singleton
      accessor in a world where a window is not, and every one of them had to say *which window*; adding that later
      would have changed the signatures applications call. Built 2026-10-11 and shaped by W1 ([what W1
      found](architecture.md#what-the-two-window-witness-found)), all of it not main-thread and safe from any thread:
      - **`Windows`**, a root from `GUI`. `windows.named(NamedWindow...)` declares a second window: the framework
        builds its tree at once (a `Gui` on a bus of its own and the application's lanes, the look, a title bar)
        and claims it at `ATTACH` (placement and zoom remembered under its name, the window mark, the bar's
        controls and instruments, the clipboard). `windows.main()` is the main window, whose `close` is quitting.
        `windows.views()` is what `Driver` reads. The calculator's settings window, Vexplore's viewer and W2's
        tool window each lost a `@MainThread` recipe (calculator FN-1, FN-2).
      - **`Dialogs`**, a root from `CONFIG`: a `Modal`, and NFD's open, folder and save pickers, parented to the
        main window and answered on the handler lane. A question asked before there is a window is held and asked
        once there is; a `@Provides Dialogs` (or `shell.dialogs(...)` by `WINDOW`) replaces the framework's, which
        is how a test answers. The text editor's `NativeDialogs`, Vexplore's `Chooser` and Pix's `Opener` were
        three copies of one class, each taking the `GuiApp`, and are gone (Vex FN-6's dialog half, FN-7).
        `Modals` stays a static, for MainFrame, which is hand-wired.
      - **`Window.onClose(CloseGate)`**, a close gate on any window, the main one included: two callbacks rather
        than gui-core's `CloseRequest`, which a test could not construct, so the gate a test calls is the gate the
        window calls (Vex FN-6's other half). Once per window, read when the close arrives. `Shell.onClose` is
        deleted; the template and the text editor arm `windows.main().onClose(actions::guardClose)`, and W1's
        palette asks before it closes.
      - **`memory()` is deleted**, accessor and root; **`app()` stays**, as the device (the ruling under *what
        `Shell` hands over* above).

      **Left: reopening what was open.** `WindowMemory` records whether a window was open (`wasOpen`), and nothing
      uses it, so a window open at quit is closed at the next launch. A choice to make deliberately, per window
      (`NamedWindow` is where it would be said), rather than a default to inherit. Additive, so not v1-blocking on
      its own; it stays here because it is the seam's.

- [ ] **Spike the extension test.** [v1.md](v1.md#how-future-proofness-is-measured): for each row of the
      extension table, a throwaway prototype against the API as it stands, and the number of existing
      signatures it forces to change recorded here. Candidates: the second window, a Linux backend,
      `@Subscribe`, supervision, a second starter. Zero is the target; anything else is fixed before the
      freeze. Do it with the witnesses, since both need the same applications.

      **Done 2026-10-02: the tree, 0.** A throwaway `@Component(parent = ...)` (an attribute defaulting to none; a child
      shares its parent's lane unless it names one; the parent is built first; a non-component parent or a parent
      cycle is an error) changed no existing signature and none of the 67 existing processor tests; `Shell`,
      `Placements`, `Lanes` and the runtime were not touched, since a lane is a string. Not spiked: child-before-
      parent shutdown, which falls out of reverse construction order but was not run, and supervision naming a
      subtree. The other rows (Linux backend, supervision, second starter) are still to do.

      **The second window, 2026-10-11: built rather than spiked, 2.** `Windows` landed for real (W1), so the count is
      of the actual change: two existing signatures went, `Shell.memory()` and `Shell.onClose(Consumer<CloseRequest>)`,
      both deliberately and before the freeze, each replaced by a per-window form (`Windows`, `Window.onClose`).
      Everything else was additive: a new root, new types, no recipe signature forced to change.

- [ ] **Build the liveness guarantee.** Designed in
      [architecture.md](architecture.md#a-wedged-component-cannot-freeze-the-window), contract in
      [v1.md](v1.md#the-liveness-guarantee). **Landed, all tested with no GPU:** each component thread in its
      own `ThreadGroup` with `Lanes.interruptLane`; a per-lane busy gauge (`Lanes.busy`, set around every
      `Placement` delivery, so parked is idle and *inside one delivery too long* is a stall); `Watchdog` in
      `-core`, a thread on no lane that reports each stalled delivery once and ends the process with
      `Runtime.halt` (status 1) when a deadline passes; `Disposer.beforeClose`, which arms a total bound on
      shutdown; and `LivenessPolicy`, handed back with `shell.liveness(...)` or a `@Provides` returning it, with
      a default for every part. **Defaults, not contract:** stall threshold 10 s, shutdown bound 10 s, exit grace
      30 s (the time a close gate's save prompt has before the process is ended anyway), and on a stall report
      then exit. All three durations are the policy's to override.

      **Still to do:** the main thread is not watched, on purpose. It legitimately blocks in a modal native
      dialog for as long as a person takes, and a halt fired at someone choosing a filename is worse than the
      wedge, so it wants a heartbeat that knows a dialog is up, from the frame loop. The processor check that
      main-thread code sends only on channels that never block is built (T4.7) for declared sends only; see
      the `@Subscribe` entry. Cancellation tokens on `Lanes.offload()`, and
      the handler and offload pools are not grouped. `Context` has `exit` and `interrupt` and no `restart`.
      Verify the group sweep under native-image, untested. W2 gets the *wedge* button and the assertions that
      the window keeps painting and close exits within the bound.

- [ ] **A look is colour; shape, density and type are each widget's** (upstream, `vexelray-gui` and
      `vexelray-text`). Found by Vex's retheme to a design mockup (FN-14 to FN-17): every colour landed through the
      palette, and nothing else did. Corner radii, border widths, padding, row heights and text sizes are literals in
      each widget, some repainted on every state change so they cannot be restyled after construction, and a role
      override is global (a quiet `LINE` for the cards quietens the dialogs too). The atlas is baked from two
      Noto faces, with no third face, weight, slant or tracking, and it lacks glyphs applications need (the
      calculator spells `≤` as `<=`, FN-3). `Span` carries a colour and no style. Vexplore's FN-6 is the same
      finding from the palette side. **Why this blocks v1:** how an application declares its fonts is build shape,
      which v1 freezes; `Node.font(int)` is the one text-styling call; `Span` is a record, so adding a style
      component later breaks its constructor and every deconstruction pattern; and turning widget literals into
      token reads moves every application's pixels. Shape and density tokens on `Theme` are additive (an interface
      with defaults); the fonts and `Span` are not.

- [ ] **A legacy sweep: v1 keeps nothing for backwards compatibility.** The second v1 condition, in
      [v1.md](v1.md#what-must-not-remain). Known candidates so far, each a question and not yet a finding:
      the prior constructors and `Config` forms the engine refactor kept on purpose; `--profile`, which parses
      and nothing honours; `shell.wake(gui::onWork)`, which duplicates what `GuiApp` already does; the
      `VexelApplication.run` overload taking a window factory, which looks like a test seam; `Gui`'s
      `hasPendingWork` (no caller), `keyRoutes` (no caller) and the `frameOwed` overlap; `Modals`' one-argument
      `install`, documented as a library default; the stale `sealed ... permits` in vexelray-gui's
      architecture.md §6; and the *deliberately not bounded yet* handler lane.

      **The imperative path is gone (ruled and done 2026-10-01, [components.md](components.md) rulings 1 and 2):**
      `@Subscribe` is the one way, `Shell.place`, `Placement`'s constructor, `start` and `subscribe` are package-private,
      and `Placement.superseded()` is deleted. What is left of the sweep is the list above, and it binds the siblings
      `Shell` exposes.

- [ ] **The public surface has not had a freeze pass.** When the rest of this section is done: read every
      public type and ask whether anything would be renamed or restructured today, fix it, then mark what
      is frozen. This is the step that turns *nothing known is wrong* into 1.0. It cannot start early,
      because it is meaningless while something above is open.

## Can land after v1

Additive: a constructor parameter with a default, a test, a deletion that changes no behaviour, a new
module behind a seam that already exists. Worth doing, and none of it waits for the freeze.

- [ ] **`Relaunch` has only been seen under the JVM** (`-shell`, 2026-10-19). In a native image it restarts the image
      with its arguments and never calls `ManagementFactory`, but the call is still reachable, so build a native
      release and restart it in debug mode once. The JVM path rebuilds the launcher from the input arguments, the
      class path and `sun.java.command`, because `ProcessHandle.Info.arguments()` is empty on Windows; the process's
      own `GetCommandLineW` (vexelray-os already reads it, in `CommandLine`) would be exact on both, behind a
      `NativePlatform.commandLine()`.

- [ ] **A light/dark switch in the title bar, for an application that declares both looks** (`-shell`'s
      `Appearance` and title bar, probably as a `WindowInstrument`; `-template`). Asked for 2026-10-21, after the
      scheduler retrospective.
      - **Ruled 2026-10-21: the application authors both themes, and the framework derives neither.** Reason:
        writing a design system takes under five minutes, and the two hand-tuned pairs on the stack
        (`Palette.DARK`/`LIGHT`, Vex's `Look`) show that a light theme is not a reflection of a dark one. The
        light page sits at L 0.975, where a mirror gives 0.809. The accent goes from 0.6925 to 0.56, where a
        mirror gives 0.31. Shadow alpha goes from 0.55 to 0.18. A derived theme would be a guess that somebody
        then tunes by hand.
      - So the button appears only when `Appearance` carries a second theme, and an application with one theme
        has none. `Appearance` is `EXPERIMENTAL`, so a new component behind a wither is additive.
      - The choice is remembered beside the zoom (`WindowMemory`). It applies to every window that took the
        application's theme, never to one with its own (`applyTo`'s rule). The swap is `Gui.theme`, which
        fades under a `PaintMotion`, so nothing changes in one frame.
      - **The catch is a colour kept as a `Color` rather than a `Role`**, because a retheme cannot reach it.
        The template's own `Look.EMPHASIS` is one, and so are Vex's scope colours, which is why Vex's theme
        menu only takes effect at the next start. The template should make each of its own colours a role, and
        test each role's contrast against the page in both looks.

- [ ] **The camera records as well as photographs, for a person and for ottermate** (`vexelray-gui-core`'s
      `WindowInstrument` and `WindowControls`, `vexelray-gui-nfd`'s `SaveScreenshot`, `vexelray`'s presenter,
      `ffmpeg-wrapper`, ottermate). Asked for 2026-10-21, after the scheduler retrospective, which had to build a
      slow-motion switch, burst screenshots and a frame-step script to check that nothing changes in one frame.
      - **What the user sees:** the camera opens a menu (`WindowInstrument.menu`, the seam the automation plug
        already uses): *Take screenshot*, and *Record video to file…*. While recording, the mark and the tooltip say
        so (`Live`) and a click stops it. Later, *Take stills of contentful redraws*.
      - **Recommended, not ruled: one frame tap, three sinks.** A tap hands every frame the window *presents*, with
        its present time, to a sink. The sinks are a video file, a still per redraw, and ottermate's `film`
        (largest step between frames, the retrospective's spike detector). `capture` cannot be the tap: it forces
        a render and reads it back synchronously. A recorder must not cause frames, and must not stall the loop.
        So the readback copies into a ring of staging buffers and is mapped a frame or more later, on a worker.
        That is a presenter feature in `vexelray`, inside the frame budget.
      - **Decided 2026-10-21: AV1, in Matroska/WebM.** Why it fits (the recommendation's reasons, not yet weighed):
        it is royalty-free, with BSD-licensed encoders, where x264 is GPL. It has coding tools made for screen
        content, palette mode and intra block copy, which are what flat UI and text need. And Matroska stamps every
        frame with its own time, so a loop that presents nothing while idle is recorded as it happened, with no
        repeated frames. The pinned build already held the encoders and the Matroska muxer, so no library was
        added.
      - **Built 2026-10-21: the encoder, SVT-AV1 in real-time mode, with the GPU's as an option.**
        `ffmpeg-wrapper`'s `Ffmpeg.record` (a `Recording`: RGBA8 frames with their times in, AV1 in `.mkv` or
        `.webm` out) and `vexelray-codec-video`'s `Recorder` (offered frames from any thread, never waits, encodes
        on its own thread, drops and counts what it cannot queue). At 1080p it takes about 7 ms a frame in software
        and 4 ms with NVENC, on the Core Ultra 7 255HX laptop. `Encoder.HARDWARE` tries NVENC, then Quick Sync, then
        AMF, and falls back to SVT-AV1. libaom was ruled out as too slow. rav1e was not measured. Windows is built
        and tested. The Linux build script compiles the encoder in too, but has not been run on Linux yet, and
        VAAPI is not wired, so Intel and AMD GPUs on Linux record in software. Whether SVT-AV1's screen-content
        detection engages on a real window is not measured.
      - **Decided 2026-10-21: a window resized while recording stays on one canvas.** The recording is opened at
        the screen's size, and each frame goes at its top left, unscaled, on black. A larger frame is cut to fit.
        AV1 can change size from frame to frame, but Matroska's track header holds one size and players differ on
        which they believe. The canvas plays everywhere. Its black added 4% to the file, but the encoder works on
        the whole canvas, so CPU follows the screen's size, not the window's: about three times as much for a
        quarter-screen window. The tap
        hands over each frame at the window's size and the screen's size at the start, and the recorder handles
        the rest.
      - **Built 2026-10-21: the frame tap, the camera's menu, the save dialog and a `record` verb.** A person can
        start and stop a recording, and so can a script. What is where:
        - `vexelray`: `WindowPresenter.tap(FrameTap)`, and `FrameTapRing`, shared by `DxgiPresenter` and
          `WindowedPresenter`. Each presented frame is copied, in its own command buffer, into one of three mapped
          host-cached buffers, and handed over in order once its fence has signalled (`vkGetFenceStatus`, which
          never blocks). A frame that finds every buffer held is skipped, not waited for. The swapchain now asks for
          `TRANSFER_SRC`. `PresenterTapTest` covers the swapchain path on a real window.
        - `vexelray-gui-core`: `WindowControls.tap` and `workArea`. `WindowRecorder` is the seam a codec module
          fills through `ServiceLoader`, so the GUI links no encoder. `WindowRecording` holds the process's one
          recording: start, stop, the camera's `LIVE`, a file finished when its window closes, and a shutdown hook.
          `WindowInstrument.camera` and its filled-lens mark. An instrument's `menu` is now told its window.
        - `vexelray-gui-nfd`: `SaveRecording`, the menu (*Take screenshot…*, *Record video to file…* or
          *Stop recording*) and the save dialog. `SaveScreenshot.instrument()` returns it when a recorder is on
          the class path.
        - `vexelray-gui-automation`: `record <path>`, `record stop` and `record`. There is no ottermate `film`
          sink yet.
        - `vexelray-codec-video`: `WindowVideoRecorder`, encoding BGRA straight from the mapped buffer, with the
          last frame held until the stop (`Recording.repeat`).
        - `ffmpeg-wrapper`: a cluster closes every second and every packet is flushed, so a killed process leaves a
          file that plays.

        Checked on the video player under ottermate on Windows (DXGI): 1080p video in a 1280x760 window, 182
        frames in 7.3 s, none dropped. The frame recorded at the moment of a `shot` matched the screenshot at 38 dB.
        Stopping from the menu, closing the window and killing the process each leave a file that plays. **Not
        run on Linux.** The native save dialog was not driven, because a background run must not raise one at the
        person, so *Record video to file…* has been clicked by no one yet. `-Dvexelray.vulkan.validation` was
        not available, so the copies have not been checked by the validation layer.
      - **Asked 2026-10-21: a stepped recording.** Kronometer drives the clock while recording, and each
        presented frame advances logical time by exactly one video frame, however long it took to draw, read back
        and encode. The window may stutter for the person watching it, but the file plays perfectly smoothly.
        `KronoGui.tick(Dur)`, "for a scripted or headless run", is the seam, and a stepped frame can afford a
        synchronous readback because the frame budget no longer applies.
        - **Prerequisite: one clock for everything that moves.** `InputDispatcher` steps the wheel glide and the
          focus ring's fade from `System.nanoTime()`, not from the kernel, so a stepped recording would play them
          at the wrong speed. The scheduler hit the same thing in slow motion (its FN-7, and the retrospective's
          global time scale). What is drawn reads the frame's time. Key repeat and double-click stay on the wall,
          because they measure a hand.
        - **The catch is input.** Logical time drifts from the wall, so a person's own timing is warped: a pause in
          which twenty slow frames were drawn plays back as a third of a second. A script driving in logical time
          (ottermate) does not have this problem.
        - **Recommended, not ruled: idle runs on the wall, motion runs on the step.** While nothing is animating,
          logical time follows the wall, and Matroska's per-frame times keep the pause as long as it was. While
          something is animating, time advances one step per frame. A person's pauses survive, every animation is
          smooth, and the only cost is that input arriving mid-animation waits for the next stepped frame.
      - **Open: when the stills mode takes a still.** A *contentful* redraw is a presented frame whose pixels
        differ from the last one. Many animation frames will qualify, so it wants a cap or a minimum step.

- [ ] **The template's navigator marks its row actions with text glyphs** (`Navigator.java`: `"›"` for Open,
      `"»"` for Show this folder). Since 2026-10-20 an icon the suite's set lacks is an application's own baked
      set (`src/main/icons`, the pom's `app-icons` profile, `vexelray-gui-icons/README.md`), and Vex's navigator
      already moved: `Icon.CHEVRON_RIGHT` and its own `TextEditorIcon.MAKE_ROOT`. The template should follow, which
      means either shipping art in every new project or a `make-root`-like icon joining the suite's set; which one
      is the question. The rest of the stack's glyph-icons are listed by app: the inspector's fan (a drawing in three
      metals, animated, so not a one-colour field), Vex's and the template's unsaved dot, and PictureViewer's Loop,
      which borrows `REFRESH`.

- [ ] **The template's `.gitignore` is five lines** (`-template`, `vexel-desktop/files/gitignore`): `target/`,
      `capture*.png` and three IDEs. It misses `.claude/`, which every sibling repo ignores by hand, along with the
      JVM's crash and profiling dumps, the native build's leftovers beside `target/`, and OS files. Found
      2026-10-20 in `vexelray-scheduler`, whose root `.gitignore` (`49a16b5`) is the one to copy: sections per
      source, `!**/src/**/target/` as `vexelray-gui` has it, and captures by name rather than a blanket `*.png`.
      Its Lean section is that repo's own and can stay there. The applications already generated (mainframe, Vex,
      the calculator, Vexplore, the inspector) keep their copies until someone updates each by hand.

- [ ] **`VEXELRAY_AUTOMATION` opens the socket but does not set the log mode** (atchung `LogConfig`, or
      `-shell`'s `Logs.start`). `Driver` reads the variable since 2026-10-18; `LogConfig.systemSetting` maps the
      `automation` key to an environment variable named `AUTOMATION`, so a run asked for only through the
      environment logs in DEV mode rather than AUTOMATION. Decide whether the variable should also make a release
      binary, which has no socket, log as loudly.

- [ ] **Settings and the session are one file per application, written whole, and nothing stops two processes
      overwriting each other** (`vexelray-gui-core`: `Settings`, `AppHome`, `WindowMemory`). Found 2026-10-05
      planning the vexelray suite (the editor, vexplore and mainframe in one install, each spawning the
      others, one new window per spawn). `Settings.save()` writes every key it holds, from memory, to
      `~/.{app}/settings.properties`: atomic and thread-safe, but only within one process. `WindowMemory`
      keeps window placement and zoom in that same store, and an application keeps its open files there too.
      So two windows of one app each save their own stale view of every key and the last to close wins, and
      both write the same fixed `settings.properties.tmp`, so concurrent saves can clobber each other's temp
      file as well.
      - **Ruled 2026-10-05:** only an instance launched without arguments reads or writes the session
        (window, folder, open files). One started with paths, as a spawn from another app is, starts clean
        and saves nothing, or two windows overwrite each other and a one-file window drags the last project's
        tabs in. Wants an *ephemeral* switch (a `Shell` option, defaulting to today's behaviour) that
        `WindowMemory` and the session keys honour. The editor's side is in its own `docs/TODO.md`.
      - **Not ruled, recommended:** split general settings (theme, font, keybindings: shared by every window,
        wanted by every app) from the session (per instance). Save the settings by taking an OS lock
        (`FileChannel.tryLock` on a sidecar `.lock`: a created-if-absent lockfile goes stale when a JVM dies,
        and locking the file itself blocks the rename on Windows), re-reading, applying only the keys this
        process changed, writing a uniquely named temp file and moving it into place. The lock is short and
        never held across a dialog. Other windows may pick a change up from a `WatchService` or on focus;
        optional. Shared settings want a home shared across applications, where `AppHome` today is per
        application (`~/.{name}`), e.g. `~/.vexelray/`.

      **Why after v1:** `Settings`' accessors and `save()` keep their signatures, the lock and merge are inside
      them, and the ephemeral switch is a new option with a default. The *`Window` seam* did not move it:
      `shell.memory()` is deleted and placement is per window inside the framework (`Windows`), so no application
      holds the store's session. **It moves to Blocks v1** if a shared home changes `AppHome.of`'s contract.

- [ ] **A shipped application and a drivable one are two builds, and each app now copies the arrangement.** Found
      2026-10-05, removing the console window from the native executables. The automation facility (`ottermate`)
      announces its port on stdout, so the first native builds were console-subsystem programs and a desktop
      shortcut flashed a console window; it also meant a shipped binary could open a driving socket. All three
      apps (text editor, vexplore, mainframe) now build two editions, each with the same shape:
      - **`-Pnative-release`:** `target/<name>.exe`, linked `/SUBSYSTEM:WINDOWS` with `/ENTRY:mainCRTStartup`
        (the image's entry is the C `main`, not `WinMain`; the PE header reads Subsystem 2), and the automation
        dependencies set to `test` scope through an `automation.scope` property, so the generated wiring does
        not contain them. Checked: with `--automation=0` there is no listening socket for the process, and the
        strings `AutomationServer` and `AutomationStarter` are in none of the release exes and in each debug one.
      - **`-Pnative`:** `target/<name>-debug.exe`, console subsystem, automation present, driven with `ottermate`
        and a screenshot taken for each app (the replies and byte counts were checked, not the pictures).
      - **What it cost each app:** a second source root per edition (`src/edition-debug`, `src/edition-release`)
        added by a build-helper property. Because the framework names starters in the `@VexelApp` annotation on
        the main class, vexplore and the editor each moved that annotation into a small class
        (`VexploreApp`, `TextEditorApp`) of which each edition has its own copy, differing only in `starters`;
        mainframe moved its socket code into a package-private `Driving` class that is empty in the release
        edition. The generated wiring was renamed with it.
      - **Not ruled, recommended upstream:** a parent-pom or BOM profile pair carrying the linker flags and the
        scope switch (the flags are now written three times); reachability metadata in the backend jars (see the
        *native-image metadata* entry, which also removes the trap vexplore fell into, a missing
        `-H:+SharedArenaSupport`); and a way to name a starter that depends on the build, so an application
        does not need two copies of its `@VexelApp` class. Substituting a source root from Maven is the
        awkward part of today's arrangement.
      - **Unverified:** that no console window appears for an instant. Only child `conhost` processes were
        looked for, not the window list. And `exec:exec` was not run after the change; `mvn test` passes.
      **Why after v1:** the profiles are build files, not an API. **It moves to Blocks v1** if the answer to
      *a starter that depends on the build* changes `@VexelApp`'s contract (a conditional `starters`), since that
      annotation is the first thing every application writes.

- [ ] **The full mailbox dump on a FAIL overflow, beyond the report that now exists.** A full FAIL mailbox halts
      the process on purpose ([components.md](components.md), ruling 5). **Done 2026-10-01, in atchung:**
      `MailboxOverflow`'s message now says who published the event that did not fit (thread name and the
      calling frame), when, and when the consumer last took a batch (or that it never has, and for how long since
      it subscribed), with no payload; `Faults` already writes that message to the log file and flushes before
      halting, so a framework change was not needed. The two readings it separates: drained a moment ago means the
      mailbox is too small, not drained for seconds or ever means the consumer is stuck. **Not done, and parked
      deliberately:** a separate file with every other mailbox's depth, each lane's busy state and every thread's
      stack grouped by lane (the consumer's stack is the "why"). If it is built: the overflow is detected on the
      publisher's thread holding the mailbox lock, so the writer has a deadline (about 2 s) and the process halts
      regardless, other mailboxes are read with a try-lock, and the file is forced to disk first. The watchdog's
      and the shutdown bound's halts leave no record either and could share it. A per-mailbox overflow handler
      was designed and dropped; see ruling 5. **A generated project needs the new atchung installed to see it.**

- [ ] **There is no one way to put a 3D viewport in a window, and every application re-derives one.** The
      pieces exist and none of them is *the* pattern. `GuiApp.viewport(w, h)` hands out a render target and
      `Node.image(target)` shows it; the engine has `RenderTechnique` and `SampledTechniqueHost`; `vexelray-gui`'s
      `CLAUDE.md` says the application "brings the technique". What is missing is the statement of how, so each
      application chooses again, and they have chosen differently:
      - `vexelray-designer`'s `Viewport` uses `target.pipelineFor` and `renderInto` directly, owns its orbit
        camera (`gui-plot`'s `Camera`), resize policy (re-mint at a 1.35x growth threshold), input (drag, wheel
        through the bus) and axis swap, and overlays a `Sketch`.
      - `vexelray-sim-fluid`'s `DebugView` does the same by hand for a 2D field, with a fixed square target and its
        own storage buffer, and does not resize at all.
      - `SdfRaymarchTechnique` and `SampledTechniqueHost` are the *named* route and cannot be used by either,
        because neither can bind an application's storage buffer (`SdfRaymarchTechnique` builds its pipeline with no
        descriptor layouts). So an application that wants data in the march drops below the technique seam to
        `SdfComposer.fragmentSpirv(...)` with a hand-written `sdf(vec3)`, as `ConeField` does, and then has
        rebuilt the host.

      Found when asking how to render the 3D fluid (48^3 MLS-MPM) with the SDF technique: the answer was a
      survey of three repos rather than a call. What each application re-solves, and so what a pattern has to
      say once: who owns the target and its resize; how an application's data (a storage buffer, a texture, push
      constants) reaches a technique; the orbit camera and its input, including the y-up/z-up swap; overlays and
      depth against the rest of the GUI; and which thread and frame stage drives it (`renderInto` blocks on its
      own fence, so it is main-thread, `FrameStage.APP`). The fluid adds a case the pattern must not rule out: the
      data lives on a *different device* (`ParticleSimulation3` owns its own `Accelerator`), so it arrives by host
      readback each frame until the engine can share buffers, which belongs in `vexelray`.

      **What would close it:** one `Viewport3D` component, or a template in the project builder, that owns the
      target, resize, orbit and overlay, and takes a technique that can declare the buffers it binds; plus the
      technique seam growing that declaration. Where it lives is the open question: the lifecycle is this
      repo's (`Shell`, frame stage, disposer), the target and camera are `vexelray-gui`'s, and the binding is
      `vexelray`'s `RenderTechnique`. Decide which repo owns the contract before any of them is written.

      **Why after v1, not before:** a component that applications opt into breaks none that compiled without it.
      It moves to **Blocks v1** if the answer to "where does the binding declaration live" turns out to change
      `RenderTechnique`'s signature, since that is what an application's technique implements.


- [ ] **`-diagnostics`, and move `FpsProbe` into it.** It is `text-editor-vexel-demo`'s, about 250
      lines, and generic apart from the one thing that makes it worth having: it deliberately pokes
      each wake path — a timeline post, a node mutated off the frame thread, a handler that changes
      nothing — to prove each still produces a frame. That is what `--profile` should turn on, and it
      is the module the architecture doc already lists as the Actuator analogue. Until it exists,
      `profile` is a name `Launch` reserves and nothing in the framework honours — documented on
      `FRAMEWORK_KEYS` and in
      [architecture.md](architecture.md#the-two-reserved-keys), along with why the real fix is
      `@ConditionalOnType` and not a runtime check.

      **The module can land after v1; its name cannot wait.** A module name is Maven coordinates an application writes into its build, so it is chosen before the freeze even if the module is not. **The name is now taken upstream.** `../vexelray` ships a `vexelray-diagnostics` for a different
      thing — the channel a seam uses to say it silently dropped a capability (see **Next**). Two
      concepts, one name, one stack; this module wants a name of its own before it is written.

      **And most of it may already exist.** `atchung-probe` is described as *"the stack-wide profiling
      seam: off unless asked, free when off, and dependency-free so any layer can take it without
      taking the bus"*, with spans, counters, a resource ledger, a CSV correlation mode and a
      `CsvView` that hunts a run for stalls. `Pump` instruments itself with it and `Automation`
      already imports `Probe` and `Lane`. So `--profile` may be a few lines turning `Probe` on rather
      than a 250-line port, keeping from `FpsProbe` only the part that is genuinely its own — that it
      deliberately pokes each wake path to prove each still produces a frame. Worth checking before
      porting anything, and more so under the concurrency model, where *"why did we miss a frame"*
      becomes *"which component's mailbox"* and a probe already threaded through the bus is what
      answers it.

- [ ] **A second automation socket, if a second application ever wants one.** `Driver` binds one, for
      the `Gui` the framework built. `vexelray-designer` needs two — `tree` on the first does not list
      the viewport and `shot` on it photographs the wrong window — and places the second itself at
      `Driver.port() + 1`, so it never parses the flag but cannot ask the framework for the socket.
      The shape would be a `Driver.open(shell, gui, controls, offset)`. **Not yet**: one application
      is not a census, and the two things that make the designer's second driver awkward (a named
      window's controls arrive after the driver starts, and are replaced when it reopens) are facts
      about that window rather than about starting an application. Recorded so the second witness is
      recognised as one.

- [ ] **An application that configures a non-default zoom range will disagree with the text editor's
      other two windows.** `FolderWindow` and `EditorWindow` apply `Appearance.ZoomRange.DEFAULT`
      rather than the running application's, because both also run under MainFrame where there is no
      `Shell` to ask. Correct today, since nothing configures a range; wrong the moment something
      does. The fix is a constructor parameter defaulting to `DEFAULT`, and it is not worth the churn
      on two library classes until a host wants one.

- [ ] **`shell.wake(gui::onWork)` is redundant, and the framework should probably stop making the call.**
      `GuiApp.wireAllWakes` already does `gui.onWork(this::postWake)` for **every** tree it presents,
      re-checked each iteration, and says why: *"an application has more trees than it has main
      windows"*, so left to the application it is *"a line to repeat per window, correct on the window
      under test and missing on the one being used."* The framework's single call is the weaker version
      of the same thing — one tree, once — and deleting it changes nothing, which `WakeSeamsTest`'s
      falsification table records. Not obviously a deletion: the line documents that the framework
      knows the wake is owed. But it should say that rather than duplicate it, or a reader takes it for
      the thing that makes the wake happen.

- [ ] **`shell.deadline(() -> krono.kron().sleepTimeout().nanos())` is not covered by anything, and the
      reason is interesting enough to keep.** Deleting it fails no test: the clock's *wake* already
      brings the loop back whenever the timeline has work, so liveness survives without it and what the
      deadline buys is parking **until** the next moment rather than being nudged to it. That is a
      latency and power property, which is exactly the class `GuiApp.idleRefresh` calls *"bounded,
      uniform, and survivable"* — real, and invisible to an assertion about whether a frame arrived. It
      wants a timing assertion (a cue at a known moment fires within a few ms of it, and the loop woke
      once rather than a hundred times) or `atchung-probe`'s FRAME lane, which already instruments the
      wake/budget/frame chain. Parked until `--profile` is a real thing, since that is the same
      machinery.

- [ ] **Three of the six dropped-capability reports are routed and not asserted.** `InputBackend.open`,
      `InputBackend.perWindow` and `Driver.open` are covered — the first two by
      `DroppedCapabilitiesTest`, which gets its absence free because no tactroller platform module is on
      `-shell`'s test classpath, and the third by `DriverTest`'s malformed port, which is the one way a
      socket fails to bind before `shell.app()` is reached. The other three need a fault that cannot be
      arranged from a test: `InputBackend.attach` wants a backend that opens and will not bind,
      `ClipboardBackend.open` wants a machine with no clipboard, and `installMark` wants
      `setApplicationIcon` to throw. Each would need a seam taking the backend rather than opening it,
      which is more API than the assertion is worth today — recorded so the gap is a decision.

- [ ] **`PointerLock`'s state machine is asserted; the line that installs it is not.**
      `PointerLockTest` drives the threshold, the focus rule, the modes and the teardown against
      `PointerLock.Device`, which is the seam the previous entry turns down in general and which was
      worth it here because the thing behind it is a state machine rather than a one-line report. What
      is still taken on faith is `gui.onPointerLock(this::want)` — that the framework subscribes at
      all. Asserting it needs the dispatcher to fire the sink, which needs a press hit-tested against a
      laid-out tree, which is `vexelray-gui-harness` rather than a unit test. `WakeSeamsTest` already
      runs a real `GuiApp`, so the pattern exists; the missing part is a node that declares
      `dragLocksPointer` and a synthesised press over it.

- [ ] **Every animation starts with one or two frames 20 to 30 ms late.** Found by the pacing measurements
      ([what they found](architecture.md#what-the-pacing-measurements-found)): on a loop whose frame gap is a
      steady 6.94 ms, the first ~60 ms of a 600 ms pulse contains one gap of 20 to 30 ms at about 30 ms in and
      often a second at about 60 ms, three or four frames dropped each, every time, the third pulse included, so
      it is not first-run warm-up. It is the one place the measurements show the loop failing the goal that
      nothing ever hiccups. **Two stalls; one is explained.** **A (~29 ms at the click)** is the 60 Hz
      ceiling parking the loop for 16.6 ms just as a `krono.ramp` waits for its first tick, plus **Windows 11
      throttling the timer** for a window that is not in front, which stretches 16 ms to 28. The second half
      is confirmed and **fixed**: `vexelray-os-windows`'s `TimerResolution` makes both calls,
      `SetProcessInformation(ProcessPowerThrottling, IGNORE_TIMER_RESOLUTION)` and `timeBeginPeriod(1)`, while any
      window exists (reference-counted per window), which took A from ~29 ms to ~18 ms. `PacingMeasurementTest`
      now asserts it: no frame gap over 26 ms in the first 100 ms of a pulse. (`timeBeginPeriod(1)` alone does
      nothing, which is why it first looked ruled out.) A refusal by Windows is kept in
      `TimerResolution.problem()` and reported nowhere yet, because the os module has no `Diagnostics` to say it
      with; the shell could ask. The rest of A is the ceiling, so it goes with the `maxFrameRate` decision. **B (~21 ms at 27 to 60 ms in) is explained, and it is not the GPU**
      ([what the stall probes found](architecture.md#what-the-stall-probes-found)): the loop runs flat out for a
      few frames after a park, because every clock tick's wake ends the ceiling's park, and the compositor's
      present queue only pushes back once it is 3 to 5 frames deep. The wait is that queue draining. The fix that
      works, and needs no platform feature, is a ceiling that holds: re-park with a timed sleep (not
      `waitEvents`, which returns at once on an unread wake message), anchored at the frame start, set to the
      display's refresh interval. Measured with 6.944 ms, every pulse after the first runs 5.4 to 8.5 ms a frame
      from the first frame. The interval now comes from the window (`maxFrameRateToDisplay`);
      what remains is a per-monitor read, and the Linux and macOS sources. (`DwmFlush` also worked and is a flag, off:
      `-Dvexelray.present.flush=true`.) **A0, new:** in the *first* pulse only, the first tick takes
      4 to 6 ms to reach the animation (`kron.ramp`'s first spork), which is most likely cold code and is not a thread-hop cost
      (each baton pass is 50 to 100 us). Ruled out for both: a JVM pause, the uncommitted `GuiApp` draft, the wake path. A per-frame
      assertion belongs in `vexelray-gui-harness` once it is understood, since a frame-gap ceiling is the
      regression test for smoothness. **Not measured, and needs engine work:** input to glass. The engine
      enables only `VK_KHR_swapchain`, so `VK_KHR_present_wait` or `VK_GOOGLE_display_timing` means enabling the
      extension and its feature chain in `VulkanDevice` and adding the wait to `WindowedPresenter`, on a driver
      that may not offer either.

- [x] **A test for the second close gate.** Done 2026-10-11, without a `GuiApp`: the gate is `Window.onClose` now,
      which refuses a second on the same window whatever the phase, and `WindowsTest` covers it headless
      (`eachWindowHasAGateOfItsOwnAndOnlyOne`); W1 drives a gate on a real window.

- [ ] **The designer's comment describes the mode the framework does not use** (fix belongs in
      `vexelray-designer`). `Viewport.java` says *"turning is a displacement, so the pointer is held for
      the gesture and warped back each frame"* — warping is `PointerLockMode.RECENTER`, and
      `PointerLock` defaults to `RAW` precisely so that nothing warps and the cursor reappears where it
      vanished. The line was written when nothing carried the intent out at all, so it described an
      intention rather than an observation. Now that it does, the comment is the only place on the
      stack that still says the cursor moves.

### Found by Vexplore

The first application built on the generated template by someone other than its authors: a file explorer
(`../vexplore`, with [its own notes](../../vexplore/docs/framework-notes.md)). Generating it, building it,
reaching a screenshot and driving it with `ottermate` all worked, and the layout came out close to the design on
the first build — so what follows is friction, not failure. Ordered by how early a newcomer meets it. None
of it would break an application compiled before it lands, which is why it is here and not above; **the first
two are the ones a new project meets before it has written a line**, and would be the first things to do if
onboarding is the goal.

- [x] **There is no route from the README to a project.** The README says what a project looks like and never how
      to get one. `new vexel-desktop` is in `mainframe`, whose `mainframe-dist` jar on disk fails with
      `NoClassDefFoundError: dev/mainframe/gui/desktop/Desktop$Apps`. What worked was a short Java program
      against `-template`'s `Catalogue.bundled()`, `Answers`, `Scaffold.of` and `Blueprint.Writing` — what
      `-acceptance`'s `Support.generate` already does. Fix: a `main` in `-template` (or a script beside it) taking
      `--in`, `--name`, `--group`, and a paragraph in the README. The `mainframe` route then becomes a second
      way to run the same thing rather than the only documented one.
      **Done:** `Generate` in `-template` (`GenerateTest`), and *Starting a project* in the README.

- [x] **The template's `Model` hands every application a race.** `Model.onChange` is `state.onCommit(...)`, which
      delivers on the committing thread *after* the compare-and-set, and the template says every handler commits from
      a worker pool. Two handlers finishing together deliver as version 6 then 5, or concurrently, so a `Ui.show(Doc)`
      written exactly as the template says can finish on a stale document, or run twice at once. Vexplore hit it as a
      rail with the wrong contents and rebuilt panels that lost their landmarks. Its `Model.onChange` now takes a
      lock and drops any document older than the last it delivered (tested with eight threads). Fix in the
      template's `Model` at least; better an ordered variant on atchung's `State` (`onCommitLatest`), since the
      shape is not specific to this framework. **Also in atchung:** `State.commit` counts a function that returned
      the same value as a new version, so re-announcing an unchanged selection wakes every listener; a
      `commitIfChanged` would remove a check every caller writes.
      **Done:** atchung's `State` gained `onCommitLatest` and `commitIfChanged` (tested there), and the template's
      `Model.onChange` is one line over `onCommitLatest`, with an eight-thread test in the generated `ModelTest`.
      A generated project now needs an atchung installed with those two methods.

- [x] **A `Wiring` keeps its parts private, so a capture or a test cannot reach the model.** `<App>Wiring` holds
      each part in a private field with no accessor. To put Vexplore in a known state for a screenshot it reads
      four system properties in `Startup`, applied after the first listing. A generated, test-visible
      `parts()` (or `Wiring.get(Class)`), returning what the `Recipes` built, would let `Capture` and a unit test
      drive the same objects a user does. Additive.
      **Done:** the generated wiring has a package-private accessor per part, named for its field (`wiring.model()`, `wiring.ui()`), typed and null before its phase; `VexelApplication.tree(new XWiring(), args)` leaves the caller holding the wiring. Not `parts()` or `get(Class)`: a lookup by class would be the reflection-shaped route this repo avoids.

- [ ] **`Recipes` can only say "after" by taking a parameter it does not use.** `Ui` needs the `Previewer`'s
      listener registered before it seeds the model, and the only way to say so is `ui(..., Previewer previewer, ...)`
      with the parameter unread. It works, and it is the right instinct that order comes from dependency — but a
      part that must come after another for *timing* and not for *data* has no name for that. `@Provides(after = ...)`
      would say it in the place a reader looks. Additive.

- [x] **The template ships `Capture.java` and the docs retire it.** `vexelray-gui/docs/reference/automation-cli.md`
      §2 and §7 call per-application `--capture` "correct about the chrome and silently wrong about the content"
      and list `mainframe-template`'s copy for removal; `-template`'s `files/Capture.java` is still generated and
      the README teaches `--capture out.png`. Vexplore has no viewport so it was right, and it was the fastest way
      to see a layout, but `ottermate`'s `shot` (the replacement) worked immediately once used. Decide: delete
      `Capture.java` and `--capture` from the template and teach `ottermate --launch ... shot`, or keep it and say
      when it lies. A POSIX-style path (`/c/Users/...`, from Git Bash) passed to `--capture` also ends in
      Maven's bare `Exit value: 1` with no message from the application.
      **Done, 2026-09-30: deleted.** Screenshots are `ottermate`'s only, now that it can set what a picture is of
      (`--zoom`, `--dpi`, `--size` in px or em, `--window`, and `windows` / `window` for several windows). The
      template's README has a *Taking a screenshot* section; `GeneratedProjectTest` drives the new verbs against
      a real window and checks the PNG is the size asked for.

- [x] **The generated `.gitignore` ignores every PNG.** `*.png` is meant for captures and also hides a project's
      `docs/` screenshots — silently, and this project began *from* a folder of them. Ignore `capture*.png` and
      `target/` instead, or add `!docs/**/*.png`.
      **Done:** the template ignores `capture*.png` and `target/`.

- [x] **Everything the template writes is one package of package-private classes.** Right for a counter and wrong
      for anything with a file system layer, so Vexplore's `files/` and `suggest/` are sub-packages with public
      types, testable with no window. Worth a paragraph in the README, *How it is put together*, on where a real
      application's non-GUI code goes and that it should be testable without a `Gui`; and possibly a second
      template that starts there.
      **Done (the paragraph):** in the README's *Starting a project*. **Done (the template):** `vexel-desktop` now
      writes a text editor, whose `TextFile` and `Markdown` are public classes in a `text` sub-package, tested
      without a `Gui`.

- [x] **Widgets that a second application wrote in an afternoon.** `Button` (with a toggle form), `Breadcrumb`,
      `StatusBar` and `SplitPane` were added to `vexelray-gui-widget` while building Vexplore, with tests, and
      `ListView.marked/looks`, `Table.headers` and `Table.onSort` extended existing ones; `Ui.button` and
      `docs/framework-notes.md` FN-0 in the template describe the world before them. **Do:** delete FN-0 from the
      template's `framework-notes.md` and replace the template's hand-rolled `Ui.button` with `Button`, so a new
      project starts on the component. The `-template` acceptance run will show whether the template still builds.
      **Done:** `Ui` uses `Button`; FN-0 is gone. The acceptance run builds and drives it.

**Second round (actions and modifier keys)** — [FN-12 to FN-15](../../vexplore/docs/framework-notes.md). Nothing here
blocks v1, and one thing worked better than expected: the processor's compile errors (a record behind `@Provides`;
`GuiApp` injected into something that is not `@MainThread`) were each right and each named the fix.

- [ ] **A part that needs the device later has no direct way to say so.** Textures need the device, which exists
      only from `WINDOW`, and a part taking `GuiApp` is itself `@MainThread` and pulls everything that takes it into
      the window's phase, so a headless capture can no longer build the tree. The way through is the README's
      clipboard shape: a tree-phase object, plus a `@MainThread @Provides AutoCloseable` that binds it to the device
      and un-binds on close (Vexplore's and Pix's `windowBinding`). It is right and roundabout; a provider that runs
      *when the window exists* and returns nothing the container needs to hold would say it in one line. Narrower
      than it was: the folder dialog it was found with (Vexplore's `Chooser`) is the framework's `Dialogs` since
      2026-10-11, which hold a question until there is a window, so only the device is left late-bound.

- [ ] **`settle` does not wait for the handler and offload lanes.** The rest of this entry landed in
      `vexelray-gui-automation`: chords in `key` (2026-10-12), and `await <landmark>` with no text, `keydown` and
      `keyup` (2026-10-18, protocol 1.2; held keys are released at `quit`). What is left is `settle` also waiting for
      the lanes to drain (see the `settle` entry under **Upstream**), so a click-settle-shot scene photographs the
      answer rather than the state before it. `Support.Driver.firstFrame` still polls `find`, and could use the
      no-text `await` once the witnesses run against an install that has it.

### Found by the rebuilt editor and calculator

Vex (the text editor, [its notes](../../text-editor-vexel-demo/docs/framework-notes.md)) and the calculator
([its notes](../../calculator-vexel-demo/docs/framework-notes.md)) were generated from the template in October 2026
and rebuilt fresh. What took their requirements unmodified is the larger half (the generator, phase inference, one
`Settings`, window memory, the title bar, the close gate's place, `Launch.rest()`, the dialogs, the logging), and an
editor with tabs, a navigator, a close gate and a remembered session has no line of application edge. What they
found that touches a frozen surface is filed above: the `Window` seam's three witnesses, `TREE`, the look, chords in
`key`, and a third trip over one bus per `Gui`. Already closed: the editor's icon (FN-2, now `@VexelApp(icon)`), the
calculator's crash on closing a second window (FN-4, see architecture.md, *the frame loop is the lifecycle*),
`Popout`'s shared bus (calculator FN-2), the stall behind a file dialog on Windows (Vex FN-7's update), and a
generated project with no motion (FN-12: the template builds `Motion`). The rest:

- [ ] **A recipe run for its effect has no shape.** Arming the close gate and restoring a session produce nothing,
      and a `@Provides` returning `void` is an error ("provides nothing"), as is one returning a record. Vex returns
      an empty marker class (`Recipes.Restored`), which the wiring stores and exposes; Pix's window binding (above,
      *a part that needs the device later*) returns an `AutoCloseable` and had to merge two late-bound parts into one
      provider because two would be ambiguous. Three applications, one missing thing: a recipe the wiring runs for effect in
      its inferred phase. Accepting `void` later removes an error, so it is additive. The messages no longer send
      such a recipe to an interface (2026-10-18): `void`, an empty record and the record rule each name the marker
      class and the `AutoCloseable`.

- [ ] **`onCommitLatest` makes a committer wait on another thread's listener** (atchung, Vex FN-4). Commit while
      holding a lock the listener takes and it deadlocks, with nothing thrown; and the order it keeps is among its own
      deliveries only, so a direct call racing a delivery lost Vex's last opened file from the saved session. The
      template's `Model` now warns about the lock. Delivering through a serial executor with the same version drop
      would mean a committer never waits.

- [ ] **A change made off the frame thread is sometimes not drawn until something else draws** (`vexelray-gui-core`,
      Vex FN-5 and FN-13). Syntax colours set from a worker appear on the next caret blink, and a tab change once
      stayed undrawn until the pointer moved; `requestFrame()` after `setSpans` did not help. Suspected: vexelray-gui
      `8a74b41` (*a frame is drawn when something changed*), not bisected. Not a framework surface, but `Shell` hands
      out `Gui`, so its draw-on-change contract bounds v1. **Read 2026-10-18, and no lost draw found:** publish, wake,
      the draw flag and the park were each traced, and `DrawOnChangeTest` (vexelray-gui `4774e69`) drives 500 bursts
      of worker-published spans through a parked loop, and fails if the wake is cleared late. The likelier cause is
      `settle` returning before the highlighter's `gui.async` has published: async work is not counted in `Owed`, and
      `shot` always draws fresh, so an uncoloured shot means unpublished spans. That makes it the `settle` entry
      below, not a draw defect. **Not checked:** the editor itself by eye (a caret blink seen by a person is not a shot).

- [ ] **Widget gaps, each found once** (`vexelray-gui-widget`, Vex FN-8, 9, 11, 13, 17 to 20). `TextField.onChange` is
      one slot and fires on span-only changes; `Tabs`' own Close cannot be stopped for unsaved work (the bar now scrolls
      when the tabs do not fit); `History.mark()` marks now, not a version; `SplitPane` cannot collapse a pane;
      the dialogs' title bar offers minimise and maximise; a late publication into a closed `Gui` throws;
      a node does not clip its own glyphs, and there is no ellipsis (`wordWrap(false)` on a label now holds); no
      current-line or active-gutter role; `TreeView` has one item per row; a sliding tab indicator is sixty lines
      every application with tabs writes. Additive; the units half of FN-20 is [units.md](units.md).

## Upstream

Cannot be fixed from this repo. The one that blocks v1 is under **Blocks v1**; these do not.

- [ ] **On a machine whose only Vulkan is dzn (Vulkan over D3D12), the window hangs open and the first frame
      fails** (`vexelray-present-dxgi` and `dxgi-bridge` for the frame; `vexelray-gui-core` `GuiApp` and
      `vexelray-os-windows` `Win32Window` for the hang). Found 2026-10-16 from a report on an older computer:
      dark window, **Not Responding**. Reproduced here with Microsoft's Compatibility Pack, dzn forced onto WARP and
      the process CPU-capped; the steps, scripts and transcripts are `vexelray-gui/docs/guides/no-native-vulkan.md`.
      Two separate faults. **The frame:** dzn makes the image the presenter shares with DXGI a simultaneous-access
      texture, then emits render-target (or, in `GENERAL`, direct-queue-common) barrier layouts on it, which D3D12
      refuses (debug layer error #1334), so `vkEndCommandBuffer` returns -1 at `DxgiPresenter.java:307`. The same
      happens on dzn over a real GPU, and dzn draws offscreen correctly (`EffectsRenderTest` passes on it), so only the
      shared image is the problem. **The hang:** the main window is shown when it is made, and nothing pumps its
      messages again until `RUN`; device selection, the device, DXGI, the atlases and `ATTACH` all run in between,
      which is about a second here and more than 5 on a slow enough machine. `Win32Window`'s comment at the
      `createWindowExW` call still says the window is created hidden until the first frame, which the code below it
      no longer does. Not a v1 blocker: no application code changes for either. **Ask:** a shared resource dzn
      cannot mistranslate (a buffer has no layouts: draw into a private image, copy it to an exported buffer, and
      have D3D12 copy that to the back buffer), at least when the driver is dzn; and pump the main window between
      the bring-up stages, or keep it hidden until the first frame, which needs deciding against *no visual
      discontinuity*. Report the barrier bug to Mesa as well, since machines will carry whatever dzn they have.

- [x] **DXGI is the default presenter on Windows, and `dxgi-bridge` had no native-image metadata** — **fixed
      upstream.** VexelRay on Windows presents through DXGI and only DXGI: `vexelray-gui-core`'s Windows profile
      carries `vexelray-present-dxgi`, and `GuiApp` will not start there without it (the swapchain is too fragile
      to fall back to). In a native image every D3D12/DXGI downcall and COM vtable call has to be registered under
      `"foreign"` (`vexelray/docs/native-bindings.md` §6), and neither `dxgi-bridge` nor `vexelray-present-dxgi`
      registered any. Without them the calculator's native binary died at startup with
      `MissingForeignRegistrationError`. Both now ship `reachability-metadata.json`. They are written from every
      `FunctionDescriptor` in the source, not only from a trace, because COM methods bind lazily and
      `DxgiSmokeTest` reaches only some of them; the trace was used as a cross-check.

- [x] **With two windows and none focused, the loop can park for good with a wake outstanding**
      (`vexelray-gui-core` `GuiApp`'s loop) — **fixed upstream** (`vexelray-gui` `809c584`, with
      `TwoWindowsWakeTest`). Found by W1; the trace and the reading are in [what W1
      found](architecture.md#what-the-two-window-witness-found). A wake that lands during a frame sets the tree's
      *frame owed* flag and posts a message. When more than one window pumps the thread's queue in that iteration,
      the message can be consumed before the park, and the loop then parks `forever` because nothing is focused,
      while every later wake is suppressed because the flag is still set. A component's result arriving into an
      unfocused multi-window application is never drawn. **Ask:** do not park while `wakePosted` is set (it is
      already recorded, and read only for the frame ceiling), beside the existing refusal to park on a queued task.
      That is what landed.

- [ ] **On macOS, a modal dialog on the GUI thread is reported as a stall** (`vexelray-gui-core`
      `GuiApp.reportStall`). The screenshot button opens a native save dialog (`vexelray-gui-nfd`'s
      `SaveScreenshot`, through `FileDialog.saveAsync`). On Windows that runs on the module's own dialog thread
      and the window keeps drawing, so nothing is reported. On macOS AppKit requires the window's thread, the
      dialog blocks it, and the first one in a process takes more than 250 ms. So `GuiApp` warns once that a task
      stalled the window, and advises the offload lane, where the dialog cannot run. **Ask:** a way to post work
      that is modal by design (`GuiApp.postModal`, say), which the stall check leaves alone, so the warning stays
      reliable for work that should not block.

- [ ] **The layout has no wrapping row, and the palette has one accent** (`vexelray-gui-core`). Found by
      Vexplore ([FN-5, FN-6](../../vexplore/docs/framework-notes.md)). Chips that flow onto the next line are
      counted two to a row by hand, which is right at one zoom and wrong at another; the design wants teal for
      *selected* and amber for *the application is speaking*, and gets the second by borrowing `Palette.action`,
      whose documented meaning is the fill of a filled control. `Role` being an open functional interface made the
      application-level wash colours easy; a documented pattern or a third anchor would make it a decision.
      Also, the resize cursor (`vexelray-gui/docs/plans/todo.md` §4.2) now has two consumers, `Table`'s grip
      and `SplitPane`'s divider, and neither can say what it is.

- [x] **A texture can be made and not safely let go** (`vexelray-gui-core` `GuiApp`) — **fixed upstream.**
      Found planning Vexplore's image preview, which is `Imagelib.decode` → `GuiApp.texture` → `Node.image`, a new texture per selection.
      `texture`'s Javadoc says content that changes *"closes the old one after a frame that no longer names it"*,
      and that puts the duty on the one party that cannot see it: the device and the present fences are private on
      purpose, so an application can only guess when no frame in flight still samples a texture. Too early is a
      use-after-free on the GPU; never is a leak until exit, which is all *"closed with the application"* offers.
      Right for icons loaded at startup, wrong for anything that browses. **Ask:** `GuiApp.release(texture)`, which
      queues the texture and closes it once a presented frame no longer draws it (or a retire queue keyed on the
      frame counter, which comes to the same thing). It is the invariant an application cannot be trusted to
      re-derive, so it is the framework's.

      `GuiApp.release(texture)` is what it got, and the split it draws is the one the ask implies: the tree is
      the application's half (release after the last node showing it has another image), the GPU is the
      framework's. A released texture is held one whole loop iteration, so an edit posted just before the call has
      been drained by every window, then closed after one `waitIdle` at the top of the next — which, with one
      frame in flight, is what the next frame's fence wait would have cost. A frame counter alone would not have
      done: windows draw only on change, so an idle window's last submit is never fenced by a later one. A texture
      some window's tree still names is not closed but kept and warned about once, since a dangling descriptor is
      worse than a leak; one the application did not make is refused at the call. `TextureReleaseTest` in
      `vexelray-gui-harness` drives both orders on a real device. `viewport`'s replaced targets have the same
      shape and were left alone, having no consumer that browses.

      **The cache on top stays in the application for now.** The policy is LRU (a browser goes back to what it saw
      recently), bounded by bytes, keyed by what the application knows (Vexplore: path, size, mtime, and the box
      for an SVG), with eviction calling `release`. Decoded `Frames` and GPU textures want different budgets, which
      is one more reason not to fold them into one framework buffer. If a second consumer appears (Vexplore's row
      thumbnails count only as a second use, not a second application), an LRU texture cache over `release` is the
      candidate to extract.

- [x] **`Modals` never receives the application's theme** (`vexelray-gui-widget`) — **fixed upstream and
      taken here.** It built its own `new Gui()`, which defaults to `Theme.DARK`, so every dialog the
      framework installed drew dark whatever `Appearance.theme()` said — against a class whose own Javadoc
      promises a dialog *"drawn with the same chrome as the rest of the application"*. Invisible in
      `text-editor-vexel-demo`, whose theme *is* `Theme.DARK`; visible in `calculator-vexel-demo`.

      The signature this note predicted is the signature it got: `Modals.install(app, Consumer<Gui>)`,
      applied to the dialogs' tree *before* it is built, because a role resolves at the moment a widget
      writes a colour. `VexelApplication` now calls it with `appearance::applyTo` — the same seam the
      designer's viewport window and the editor's other two use, and the case `Appearance.applyTo`'s own
      Javadoc was written about.

      Two things worth knowing, both decided upstream. The one-argument `install` stayed, documented as the
      library default look for an application with no decision to pass on, so the wrong call is now a choice
      rather than an accident. And the dialogs re-derive their page and message colour per dialog, so an
      application that changes its look at runtime gets a correct *next* dialog — the buttons were already
      being rebuilt, and those two were the only parts that would have stayed stale.

      Guarded by `DialogsWearTheApplicationsLookTest` in `vexelray-gui-harness`, which asserts the seam was
      handed the tree the dialogs are actually built on rather than merely a `Gui`. Writing it needed a fix
      of its own (`vexelray-gui` §6.11): the harness made its first window on the calling thread and the
      rest on the loop thread, and a dialog is modal, which was enough to leave no thread able to tear them
      all down.

- [ ] **The frame ceiling holds and follows the display; what is left is its input latency and its reach**
      (`vexelray-gui-core` `GuiApp`, `VexelApplication.MAX_FRAME_NANOS`). *The entry this replaces asked whether a
      ceiling was the intent; it is, and it is built.* The ceiling was a timed park that any wake ended, and the krono
      clock wakes the loop every tick, so nothing was held to it: after a park the loop drew frames about 1 ms
      apart until the present queue pushed back ([what the stall probes
      found](architecture.md#what-the-stall-probes-found)), and an animation crawled and then lurched at its start.
      It now sleeps out the rest of the gap after a wake (`LockSupport.parkNanos`, anchored at the frame's start), at
      one display refresh read from the window (`maxFrameRateToDisplay`, 60 Hz where the window cannot say).
      **Open, in order of weight:**
      - *Input that lands in that sleep waits for it*, at most one refresh interval (6.9 ms at 144 Hz, 16.7 at 60),
        and only when a wake and not input ended the park. A Win32 event waited on beside the message queue, so a
        wake never leaves a message behind, would let the re-park be `waitEvents` again and restore *costs nothing
        in input latency*. In `vexelray-os-windows`. Not measured: how often a click lands in the sleep.
      - *The rate is the compositor's, not the window's monitor's.* Right with one display or displays of one rate; a
        per-monitor read (`MonitorFromWindow` and the display's current mode) is exact. Linux and macOS report no
        interval and keep 60 Hz.
      - *Timed parks are millisecond-grained and not locked to the vblank*, so gaps scatter by about 1.5 ms around the
        interval (5.4 to 8.5 ms at 144 Hz). Bounded and invisible beside a 20 ms lurch; it is not vsync, and
        stamping each frame with when it will be shown is the exact answer if it is ever seen.
      - *A test.* `PacingMeasurementTest` asserts no gap over 26 ms in the first 100 ms of a pulse. The old
        behaviour (21 ms) passes that too, so **nothing guards the ceiling**. A ceiling near 12 ms would, and wants the
        first pulse's cold-start gap (13 to 17 ms) excluded, or warmed.

- [ ] **`settle` says `ok` for an application that never goes quiet** (`vexelray-gui-automation`, and this
      repo's `Driver` if it grows a verb). The long-running witness's metronome ticks to itself, and `settle`
      answered `ok` between two ticks, as its javadoc says it may: it is exact about the frame loop and the
      clock and blind to application work in flight. Nothing in the socket can say *this application does
      not go quiet*, so a script that settles before a photograph of an animating window photographs it
      mid-change. Either the framework offers a way for an application to declare it is busy, which
      `settle` waits on, or the documentation says loudly that a photograph wants a landmark to wait on.

      **A second witness, and a different cause: an application that goes quiet but is not done.** Vexplore lists a
      folder on `Gui.offload()` and rebuilds a panel on a handler thread, so `settle` returns while the answer is
      still on its way and a scripted click-settle-shot photographed the state *before* the click, every time. The
      loop was quiet and the application was not. The workaround is to write one status slot last and
      `await <landmark> <text>` on it. `Lanes` already knows how deep the offload and handler queues are, so `settle`
      could wait for both to be empty as well; and `await` cannot wait for a landmark to *exist* without text,
      which is the form a script wants for "the dialog appeared".

- [ ] **The pointer lock is asked for on the press, not on a drag** (`vexelray-gui-core`).
      `InputDispatcher` calls `requestPointerLock(dragLocks.contains(...))` inside its `ButtonPressed`
      case, beside `fireDrag(START)` — so the intent arrives before the pointer has moved at all, and a
      plain click on a viewport asks for the cursor to be hidden and then released a few frames later.
      Carried out literally, that is a flicker on every click on the designer's canvas.

      Worked around downstream rather than fixed: `PointerLock` holds the intent until the pointer has
      travelled `DEFAULT_THRESHOLD_PX`, so a press that releases without moving never hides anything.
      That is the right place for *a* threshold — travel is a fact about the device, which is the
      framework's side of the seam — but it is not the right place for *this* one. The dispatcher
      already owns a recogniser that draws exactly this distinction, in `DragGesture`'s own words:
      *"distance decides whether it was a drag"*. A lock requested on the promoted drag rather than on
      the press would need no threshold here at all, and would fix it for every consumer of
      `vexelray-gui` rather than for applications that happen to run under this framework.
