Problem
Layout measurement evaluates composite bodies before a pure measurement context is established in Sources/TUIkitView/Rendering/ChildInfo.swift:183-210. Only some paths honor isMeasuring; lifecycle, tasks, observation, state cleanup, handlers, preferences, header, and status-bar registration can still mutate live runtime state.
The first-frame header sizing pass in Sources/TUIkit/App/RenderLoop.swift:215-223 performs a full render without marking it as measurement. Correction or abandoned passes can therefore start effects or leave registrations that were never part of the visible tree.
Proposed solution
- Replace the ambient Bool with explicit
.measure, .render, and .commit phases.
- Collect effects in scratch transaction state while evaluating and laying out a candidate tree.
- Commit lifecycle, task, observation, focus, handler, preference, header/status, cache-liveness, and terminal effects exactly once from the final tree.
- Discard all effects from failed, correction, or superseded passes.
- Make header/status measurement use isolated scratch collectors.
Acceptance criteria
- Framework-controlled measurement starts no tasks, registers no live handlers/subscriptions, and commits no State, storage, cache, lifecycle, or terminal mutation. Arbitrary user side effects inside
body are unsupported and must be documented or diagnosed; the framework cannot prevent arbitrary user code from performing them.
- An abandoned frame has no lifecycle or terminal side effects.
- Only commit changes terminal output and visible runtime records.
- Buffer and committed effects are identical across single-pass and correction-pass layouts.
- Deterministic phase traces cover header, status bar, preferences, focus, task, and Observation paths.
- Swift 6.0 macOS/Linux gates and DocC complete without diagnostics.
Dependencies
Depends on stable identity (#10) and keyed child traversal (#12). It requires a stable render-result boundary, not completion of the full cell-surface migration in #11.
Parallelization
Serial critical-path work because it touches RenderLoop, RenderContext, ChildInfo, effect registration, and cache lifecycle.
Commit structure
- Split this issue into small, thematic, independently revertible commits wherever the work can remain coherent.
- Every commit must build and keep its applicable tests/gates green. Do not commit an intentionally failing regression test; use local/known-issue characterization or land the test with the smallest fix.
- Keep characterization/fixtures, mechanical renames or moves, semantic changes, and documentation/migration updates separate when each step remains green.
- The issue boundary is not a commit boundary; multiple commits are expected for independently reversible changes.
Problem
Layout measurement evaluates composite bodies before a pure measurement context is established in
Sources/TUIkitView/Rendering/ChildInfo.swift:183-210. Only some paths honorisMeasuring; lifecycle, tasks, observation, state cleanup, handlers, preferences, header, and status-bar registration can still mutate live runtime state.The first-frame header sizing pass in
Sources/TUIkit/App/RenderLoop.swift:215-223performs a full render without marking it as measurement. Correction or abandoned passes can therefore start effects or leave registrations that were never part of the visible tree.Proposed solution
.measure,.render, and.commitphases.Acceptance criteria
bodyare unsupported and must be documented or diagnosed; the framework cannot prevent arbitrary user code from performing them.Dependencies
Depends on stable identity (#10) and keyed child traversal (#12). It requires a stable render-result boundary, not completion of the full cell-surface migration in #11.
Parallelization
Serial critical-path work because it touches RenderLoop, RenderContext, ChildInfo, effect registration, and cache lifecycle.
Commit structure