fix(desktop): rekey upscroll momentum gate onto rendered scroll (w4a-gate-1)

The mid-history correction's momentum-skip gate keyed on the raw
`scrollTop` delta since baseline. On WebKit, momentum is coalesced into
`scrollTop` on the compositor's own async clock, so a frame that PAINTED
still can read a large raw delta and wrongly skip a genuine above-anchor
reflow — the reversal survivor bin (the 204px-class lurch of #1662).

Rekey the gate onto the RENDERED scroll — the painted wheel motion this
frame — `renderedScroll = aboveShift - ΔtopOffset`: the anchor's painted
on-screen move with the reflow's own push removed, both terms read from
the painted DOM (getBoundingClientRect), not the possibly-ahead
`scrollTop`. A rendered-still frame now reads ~0 and the reflow is
corrected instead of dropped.

Because the gate now needs `aboveShift` to decide, the band walk runs
before the gate. To keep "under-correcting is invisible; the next quiet
frame catches up," `sumAboveAnchorShift` no longer refreshes the height
cache inline — it STAGES the refresh and returns `{ aboveShift, commit }`.
The caller applies `commit()` only past the gate, so a momentum skip
leaves the cache for the next frame; a kept correction refreshes it as
one indivisible pass (the "second observer no-ops" invariant is intact).

Arbiter contract, fresh w4a-gate-1 build, both engines:
- raf-correction HARD gate: Chromium 0 reversals, rAF fires 0 / RO 21
  (RO sole writer); WebKit 2 reversals max 14.5px (<=34 quantum floor),
  rAF fires 24 / RO no-op (residual 0.0). Ratified invariants hold.
- slow-scroll: 0 reversals both engines (felt low-velocity regime clean).
- fast-classify DECOMPOSITION self-test (WebKit): pure-reflow attempts
  median |signedShift|=18.5 but median |renderedScroll|=0.0 — the reflow
  does NOT leak into the gated quantity, and 19/28 fire. Pure-scroll
  |renderedScroll| runs up to 28. The decomposition is proven, not
  asserted. Guarded WebKit-only: on Chromium the rAF is the passive loser
  observer (RO corrects first), so renderedScroll there tracks signedShift
  by construction — characterized, not asserted.

KNOWN RESIDUAL (surfaced, not hidden): 2 WebKit survivors remain, but
they are a DIFFERENT class — signedShift=0.0 (the band walk sees no
above-anchor reflow) while the row still lurches 14.5px. The momentum-gate
survivors the rekey targeted are gone; these are cross-check/null-target
skips where the reflow is invisible to the band walk. Separate diagnosis.

Diagnostic-only `renderedScroll` is threaded through the shared result
type + emitted on the rAF probe so the fixture can prove the
decomposition; stripped/gated before merge per the file-size exit
condition. Raises the useAnchoredScroll.ts size exception 1180->1210 to
cover the arm (biome reflows the 4-prop returns); flagged in the override
comment for Eva as heavy-for-a-diagnostic.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
This commit is contained in:
npub1cc3ha7z055mu0rwwu7806t2wt8mj3pvu0uv5mfp2c50dahaqhczshdalg6
2026-07-09 01:11:30 -04:00
parent 44d9edcf2f
commit 2bd2076ebb
5 changed files with 564 additions and 41 deletions

View File

@@ -189,16 +189,23 @@ const overrides = new Map([
// (Quinn's merge-bar checklist #4), the one-clock/single-writer reasoning, and
// the build-stamp/probe contract are all documented AT their sites. The file
// was 888 lines at HEAD; this fix's code + load-bearing provenance comments
// crossed 1000. The 1140→1180 bump is the W4a chase instrumentation inflating
// crossed 1000. The 1140→1210 bump is the W4a chase instrumentation inflating
// a load-bearing hook mid-investigation: the signed-shift grow/shrink probe
// (classifier arm) and the renderedScroll gate-clock rekey (w4a-gate-1 arm),
// both carrying their provenance inline. EXIT CONDITION: once #1662's fate is
// decided, the diagnostic emits get stripped or gated and the mid-history
// corrector + band walk split into a sibling module — Eva's ruling holds the
// structural split until the arms in flight against this file settle, so it is
// NOT done mid-chase. Comment-dominated overage on a correctness fix, not
// generic debt growth. Approved override; queued to split with the rest.
["src/features/messages/ui/useAnchoredScroll.ts", 1180],
// (classifier arm, 1140→1156) and the renderedScroll gate-clock rekey
// (w4a-gate-1 arm, ~1156→1206). The gate-arm growth is the deferred-refresh
// commit path + the momentum-gate rationale + the `renderedScroll` field
// threaded through the shared result type and all four returns so the
// classifier fixture can PROVE the scroll/reflow decomposition (Eva required
// this in the arm's first run); biome reflows each 4-prop return object to
// multi-line, which is most of the line cost. FLAGGED to Eva: this is heavy
// for a diagnostic — if the decomposition proof moves behind a lighter
// gate-only probe, this drops back toward 1185. EXIT CONDITION: once #1662's
// fate is decided, the diagnostic emits get stripped or gated and the
// mid-history corrector + band walk split into a sibling module — Eva's ruling
// holds the structural split until the arms in flight settle, so it is NOT
// done mid-chase. Comment-dominated overage on a correctness fix, not generic
// debt growth. Approved override; queued to split.
["src/features/messages/ui/useAnchoredScroll.ts", 1210],
// AgentConfigPanel footer fold into ProfileFieldGroup for the config-bridge
// panel — a small overage from load-bearing UI plumbing, not generic debt
// growth. Approved override; still queued to split with the rest of this list.