Answers Eva's leg question (thread 0a496379): are the felt −240-class media
reflow snaps on scroll-up momentum-class (killed by w4a-gate-1), walk-blind
(RO-path, a separate leg), or SHRINK-class (needs Max's pre-realization band)?
Adds a committed media corpus + classifier so the answer is reproducible from
the tip on a fresh build, mirroring the jitter/fast-classify arm.
Corpus (e2eBridge `mediaCorpusBody`, channel `media-corpus`, 400 rows, 8 kinds).
Row-height error is MEDIA reserve-vs-true mismatch, not text mis-estimation,
from three in-source-verified divergence sources:
- Link-preview cards reserved flat at PREVIEW_CARD=70 but rendered taller (GROW).
- Width-clamp images: CORRECT dims, but the estimator hardcodes MEDIA_MAX_WIDTH=384
while render clamps to max-w-[min(24rem,100%)]; in a column < 384px the render is
width-limited shorter than reserved (SHRINK, no lying dim, no decode timing).
- Dim-mismatch images in BOTH directions: overstate height (SHRINK) and understate
(GROW), so the corpus CAN produce SHRINK — the GROW-dominant prediction is
falsifiable, not baked in.
No dim-less band: that path is pinned to a fixed 256px box (reserve==mount) and
provably cannot reflow. Images use a tiny inline PNG data-URL so they realize
without network. (Eva verified the mechanism chain independently in mainbase
2cc0eb53.)
Fixture (upscroll-media.perf.ts) clones the fast classifier verbatim — probeLen
append-count join, SKIP/GROW/SHRINK/UNATTRIBUTED discriminator, dev, ±2-frame
firedNear admission — and only:
- navigates to channel-media-corpus, then narrows the viewport to 450px so the
timeline column lands below 384px; asserts the measured media-container width
is < 384 so the width-clamp SHRINK source provably bites (else it fails loudly);
- adds a MEDIA-REFLOW CENSUS: for every above-anchor reflow (|signedShift|>3px),
whether it landed on a tracked in-band row and the worst (most-backward) rowMove
across it, collapsing a persistent gate-held reflow's per-rAF re-measure into one
run so a stuck 198px GROW isn't inflated into hundreds;
- asserts MEDIA LIVENESS (>=1 in-band reflow) so a zero-reversal result cannot be
a dead corpus; drops the fast arm's decomposition self-test (that re-proves the
gate mechanism the jitter arm already ratifies — off-charter here).
Result, fresh w4a-gate-1 build, BOTH engines, reproducible from this tip:
- Chromium: 0 reversals. 9 reflow runs (5 in-band), 7 GROW / 2 SHRINK. Every
in-band reflow worstRowMove >= 0 — incl. a persistent +198 GROW held 170 frames
at rowMove 0.0.
- WebKit: 0 reversals. 8 reflow runs (4 in-band), 6 GROW / 2 SHRINK. GROW runs
gate-SKIPed to rowMove 0.0; SHRINK runs (−14.5) fired-and-corrected to +0.5
(forward).
CLASSIFICATION: the media reserve-vs-true reflows are momentum/absorbed-class —
the landed w4a-gate-1 fix + correction eat them; not one renders a felt backward
snap on either engine. NOT walk-blind, NOT needing Max's pre-realization band.
Neither Dawn's GROW-likely nor Eva's SHRINK-likely prediction "wins" in the felt
sense: both classes fire, both are absorbed.
HONESTY BOUND: this is a deterministic proxy for media reflow, not a claim to
reproduce Tyler's exact frames. His live trackpad still owns acceptance; #1662
stays draft.
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
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>
The two rAF loops (fixture sampler + hook) interleave in an order we don't
control, and a late WebKit RO appends a frame after the reflow paints. A tight
[prev, this) window mis-labeled all 4 fast-corpus survivors UNATTRIBUTED when the
attempt had appended one frame late. Widening to [prev, next) resolves them to
their true class. Fast jitter corpus, WebKit: all 4 survivors classify SKIP
(fired=false, signedShift=0.0, dScroll=0.0) — the momentum-skip gate suppressed
the write on a coalesced-momentum still frame, so the reflow renders uncorrected.
Neither GROW (absorption) nor SHRINK (Max's band): the lever is the skip gate.
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Adds the discriminator Sami specced (thread 5b46582e): the escape counter is
pure rendered rowMove and residual is |aboveShift|, so neither can recover the
grow-vs-shrink topology that decides the fix. Thread signed aboveShift (rAF path)
/ signed drift (RO path) through the correction probe as a diagnostic-only
'signedShift' field alongside wouldFire; production behavior unchanged.
The slow-scroll classifier joins each reversal to the correction attempt(s) in
its frame window (append-count join, order-robust across the two rAF loops) and
classifies: SKIP (gate/xcheck suppressed the write -> uncorrected reflow),
GROW (fired, signedShift>0 -> write is the felt snap, absorption's topology),
SHRINK (fired, signedShift<0 -> reflow renders it pre-write, only smaller
per-frame realization helps). Bumps ANCHOR_BUILD_STAMP to w4a-classifier-1 so the
stale-dist guard rejects any bundle missing signedShift.
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Drives the low-velocity + momentum-tail regime Tyler reports the residual in,
which the constant-12px/32ms gate does not cover. Characterization only — logs
the reversal distribution and still-frame count, no pass/fail ceiling, since the
slow-regime floor is what we are measuring. Keeps liveness + stale-dist guards.
Honesty bound documented in-file: Playwright mouse.wheel is synthetic and cannot
reproduce a real trackpad's compositor-coalesced momentum, so a green run means
'the correction is not a large felt jump at low synthetic velocity', not
'reproduces everything Tyler feels'.
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Realizing upscroll made the reading row jump down then snap back — a
reversal frame pair, most visible on WebKit where the ResizeObserver for
a content-visibility realization delivers one frame LATE (paint N, RO
N+1). This lands design-of-record (c): a single ungated RO mid-history
corrector plus a per-rAF sampler, coordinating ONLY through a shared
row-height cache — no engine branch. The on-time observer per engine is
the sole writer:
- Chromium: the RO delivers pre-paint, so the ungated RO corrects at N
(by the anchor's own measured drift) and refreshes the cache; the
rAF then sees residual ~0 and no-ops.
- WebKit: the RO is late, so the rAF (whose synchronous
getBoundingClientRect forces this frame's layout) corrects at N; the
late RO no-ops against the rAF-refreshed cache.
The RO path is deliberately UNGATED by the rAF path's observed/above
agreement cross-check: that gate suppresses the rAF band walk's
straddler-miscount fabrication, a risk the RO does not have (its entries
are ground truth for which rows resized). Applying the gate to the RO
strangled the on-time Chromium observer and regressed it to 16
reversals. Straddler safety on the RO path comes instead from the
safe-margin reading anchor (snapshotReadingAnchor +
READING_ANCHOR_SAFE_MARGIN_PX), which sits a notch below the realization
band so a row realizing across the fold is above the anchor and summed
into the drift correctly — documented as a load-bearing invariant at the
site so a later refactor cannot strip it thinking the `changed` trigger
covers straddlers (it does not; the margin does).
Own red/green E2E fixture (upscroll-raf-correction.perf.ts), verified on
both Playwright engines against a stamped build:
Chromium 0 reversals max 0.0px RO 21 rAF 0
WebKit 2 reversals max 14.5px rAF 24 RO 0 (residual 0.0)
Asserts are a layered stack so a vacuous pass cannot slip through: a
build-stamp stale-`dist` guard, LIVENESS (some correction fired),
PRIMARY correctness (reversals <= 4, maxReversalPx <= 34, engine-
agnostic), and per-engine MECHANISM in both directions (chromium
roFires>0 && rafFires===0; webkit rafFires>0 && maxRoResidual<=0.5).
Layering self-test confirmed: disabling the ungated-RO write flips
PRIMARY red at 16 while LIVENESS stays green.
The hook is the sole cross-engine reading-anchor scroll writer and its
correctness lives in the per-site provenance comments, so it crossed the
1000-line file-size limit (888 at HEAD); added a documented, narrowly-
scoped override queued to split the mid-history corrector into a sibling.
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Quinn's correctness flag: the shipped comment framed totalDrift as the
non-recovering-under-correction accumulation (~220px), which the measured
1042.81 baseline disproves — it includes recovering-flash reversal halves
and multi-frame realizations too. Comment now states what it measures.
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Replaces the original deviation-vs-appliedScroll W3 metric with the
ratified W4 three-leg jerk contract (RESEARCH/FELT_WHEEL_GATE_METRIC_W4.md,
Quinn) and adds the whole-run cumulative anti-scroll drift instrument
Eva/Quinn asked for as the W4a A/B reference scale.
Metric (all on the reading row's per-frame rect.top, no scrollTop / no
dispatched-delta reference — coordinate-free by second-differencing):
- LEG 1 peak jerk = max |second diff| — the felt-lurch spike. Gate <= 8px.
- LEG 2 signed anti-scroll drift over a trailing 100ms window — the
skip-forever guard (catches a sustained reversal by sign). Gate <= 12px.
- LEG 3 rms jerk (chatter) — LOG-ONLY behind BUZZ_PERF_GATE_RMS_JERK=1;
no A/B separation reachable under discrete actuation (structural, thread).
- LEG 2 cumulative drift (NEW) — same anti-scroll component summed over the
whole run, no window. LOG-ONLY, never gated. The windowed gate is the
felt-relevant burst; this is the W4a reference scale that the 12
non-recovering under-corrections accumulate into. The pre-realization
band (W2) should collapse THIS number.
Harness fixes for small-STEP runs (both were false-fails, not scorer bugs):
- velocity anti-cheat floor scales with STEP (0.008*STEP_PX) — was a fixed
0.1 pinned at STEP=12, false-failed STEP=2.
- test timeout 300s -> 900s (408 events/swipe at STEP=2 is slow).
Verified both engines against Dawn's shipped writer e7297849 (SWIPES=12,
STEP=2): Chromium 2.00 / 0.00 / cumulative 0.00 (clean PASS, control);
WebKit peak 37.55 / windowed 27.55 / cumulative 1042.81 (exposed W4a
frame-late layer). Chromium cumulative = 0.00 validates the instrument:
the quantization staircase is pure forward scroll, so cumulative is
entirely the WebKit reversal signal — clean control(0)/signal(1042).
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Tyler's on-device verdict at 3d1e91ea: better but not perfect. This probe
actuates the shape of his input — small (12px) wheel deltas at slow (~32ms)
cadence, no settling — and samples the tracked reading row's rect.top,
scrollTop and mounted count EVERY rAF, so one-frame events are visible.
Flags frames whose rowMove escapes the slow-wheel staircase envelope
(reversal <= -3px, or shove >= 2*delta+3px), pairs escapes that cancel
within 3 frames (one-frame flash of a deferred correction) vs unpaired
(felt lurch), pure rect.top in the flag condition per the W4 framing.
At tip b170680d (includes W1r3 fe786ea4): Chromium 0 escapes / 3447 frames;
WebKit 81 escapes / 1754 frames — 11 flash pairs (-27.5 shove then +28
correction one frame later) + 59 unpaired single-frame jumps of exactly
35px / 41px recurring every ~6 rows. Diagnostic, non-gating.
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
The live trackpad probe (upscroll-trackpad-live.perf.ts) proved the WebKit
compensation defect against real #buzz-bugs but needs staging + a
port-forward + a member nsec, so it can never gate a PR merge. This gate
reproduces the SAME felt-lurch metric under the SAME momentum actuation on
the deterministic mock jitter-corpus bridge — no network, greppable in CI,
on BOTH engines.
Covers both lurch sources (W3 trace, thread):
- CV-realization lurch: jitter-corpus is the heterogeneous 400-row seed
whose estimateRowHeight mismatch T1.1/T1.2 already gate.
- prepend-commit-under-momentum: 400 rows > 300 CHANNEL_HISTORY_LIMIT, so
fetchOlder pages behind an until-cursor. useLoadOlderOnScroll fires off an
IntersectionObserver on a 600px-margin top sentinel — pure geometry,
input-cadence-independent — so momentum triggers it like live.
channelWindowDelayMs=1000 defers the commit so it lands frames later,
under momentum, reproducing the commit race (not an instant same-frame
commit).
Engine fidelity mirrors T1.1/T1.2: overflow-anchor forced 'none' so
Chromium reproduces the shipped WKWebView (no native scroll anchoring);
under playwright.perf-webkit.config.ts this is the real WebKit family.
Three assertions (Eva's thresholds + Quinn's W4 caveat):
- peak felt lurches (|dev|>10px, non-commit) == 0 — the wheel-fight spike.
- smooth frames (|dev|<=1px) >= 99%.
- RMS deviation across the run <= placeholder — the skip-forever guard:
a too-tight staleness skip-bound never fires the correction under
momentum, showing as a run of sub-10px deviations that dodge the peak
gate but sum to main's raw-shift drift. Peak is blind to that shape.
RED at contract 6b9203ca under the WebKit mirror is mandatory and verified:
WebKit 132 lurches / 94.42% smooth / RMS 4.24px; Chromium-with-mirror 131
lurches / 97.87% smooth. Dawn's engine-order-independent W1 fix turns it
green on both engines. Exact lurch and RMS ceilings pinned against Dawn's
first green candidate, same discipline as T1.2.
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Two live probes that run the built GUI against a real relay (port-forward
+ Host rewrite) and walk real scrollback in a real channel:
- upscroll-1px-live.perf.ts: settled steps (BUZZ_PERF_STEP_PX), scores
per-step row movement through real fetchOlder loads, measures the felt
jump at each prepend commit.
- upscroll-trackpad-live.perf.ts: macOS trackpad replica — wheel events at
~8ms spacing in finger-ramp + momentum-decay swipe profiles, no settling,
with a per-RAF sampler scoring tracked-row motion vs applied scroll.
Plus playwright.perf-webkit.config.ts: the same probes on Playwright
WebKit. This is the config that exposed the T2 compensation being
near-ineffective on WebKit-family engines under momentum input while
Chromium scored clean — Chromium-only probing cannot see the defect class
the shipped WKWebView app exhibits.
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
The committed gate certified only the CV-realization half of H1: it
terminated at the ~600px fetchOlder sentinel band, so the pagination
half — cold rows paged in ABOVE the viewport by scrollback — was proven
only by Quinn's throwaway probe, not by the gate as it runs (the
coverage gap Quinn [9] and Dawn [10] flagged, land-now/fast-follow
ratified by Eva [11]).
Root cause: when a prepend lands, scrollTop jumps UP (~100 older rows of
height added above), so `appliedTop = before - after` goes negative and
the loop's top-of-history break fired at the sentinel — before scoring
any paged-in row. On the RED baseline this break was deterministic; on
GREEN it was flaky (the prepend was scored only when the re-anchor
happened to leave the tracked row in the safe band).
Fix: distinguish the two causes of a non-decreasing scrollTop by
mounted-count. A prepend grows it; the true top does not. On a prepend
re-anchor, skip scoring that step (its motion is the multi-thousand-px
jump, not a fixed STEP notch — scoring it would poison the metric) and
CONTINUE; the next iteration re-baselines a fresh safe-band row in the
paged-in window and scoring resumes across the prepend. Assert
`prependObserved` so the gate exercises BOTH jitter sources every run.
Verified on the T3 integration tip 1fa4551a (GREEN) and the T1.1 base
32e41e78 (RED), both rebuilt:
- GREEN: prepend scored (50->200, walks all crossings), gate 0.00/0.00
PASS, 77-78 scored steps, deterministic across 4 runs.
- RED: prepend scored (50->150), coverage assert PASSES, gate still
FAILS on jitter (peak 41px > 2.0) — reds for the right reason.
The dual-sided RED/GREEN contract and the anti-cheat floors are
preserved; the change is scoped to the gate file only.
Note: gate-run R is window-dependent (Eva/Max [8]); scoring the deeper
paged-in population raises the non-gating R diagnostic from 75 to ~85.
This is NOT a cross-lane leak — the T2 writer's reserve is untouched;
R moved only because the sample window grew, exactly as the
window-sensitivity caveat predicts.
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Two of the deferred scroll-jitter seams, on top of the integrated fix train
(T1.1 gate -> T2 writer -> T3 estimator @ 1fa4551a):
- columnWidthPx: measure the timeline row column once (useElementWidth) and
thread it into timelineRowReserveStyle so row-height reserves use the real
wrap width instead of the 64-char fallback. Pre-measure zero passes undefined
to preserve the estimator's own fallback rather than tripping its min-chars
floor. On jitter-corpus this shrinks estimate-error R from 75 -> 55 peak
(better reserves = smaller realized-vs-reserved delta) while the motion gate
stays GREEN 0.00/0.00 -- R moves only in the estimator lane, the gate only in
the writer lane, cross-lane invariant intact.
- Wren #4 anchor-contract guard: a companion perf test that reads the PRODUCTION
computed overflow-anchor on the real conversation scroller (no test override)
and asserts it resolves to none. The jitter gate forces the property for its
measurement; this proves the shipped stylesheet actually ships it, so a
regression that hands correction back to Chromium's native anchoring (masking
a WKWebView-only on-device break) fails loudly.
The onRealizedHeight seam is intentionally NOT wired: T4 ground truth
(RESEARCH/GUI_SCROLL_RECLAIM_T4.md) shows the upscroll path is append-only with
no unmount, so it needs no JS height cache; a per-event-id cache is only the
deferred, narrowly-scoped T4b for the head-refetch case. Adding an unused
callback now would be dead abstraction.
Validation:
- pnpm --dir desktop typecheck: clean
- biome check on both changed files: clean
- rowHeightEstimate.test.mjs 16/16, useAnchoredScroll.test.mjs 2/2
- perf: upscroll-jitter GREEN 0.00/0.00 (R 55), anchor-contract PASS
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Dawn found the RED gate from 33e73511 was mathematically invariant to the
fix: its residual subtracted the REALIZED scrollTop delta, which includes
the fix-writer's compensating scrollBy, so the writer cancelled out and the
gate scored raw estimate error R (only T3 can move it), not motion
smoothness. Quinn and Eva re-derived and confirmed. Co-visible and any
row-vs-row differential share the disease: compensation is a uniform global
offset, so it cancels out of any quantity not referenced to the input.
Reframe (converged with Dawn/Quinn/Eva; ratified event 63f5e6e1):
- Metric is now per-notch MOTION CONSISTENCY: the reading row's viewport
motion (rowMove = after.top - before.top; NO scrollTop reference) scored
as deviation from the run median. This references the row's own motion
ACROSS notches (temporal self-reference), never a neighbour (spatial) or
scrollTop, so the writer's scrollBy survives into the metric and is SEEN.
- Actuation is a fixed synchronous step (scrollTop -= STEP) so the input
delta is constant by construction, bypassing Blink's wheel-scaling (a
wheel notch applies 218/220/222...; median-of-run would misread that
per-notch scaling as jitter). Dawn independently confirmed even the
actuated scrollTop delta is post-writer contaminated (48-75px spread
under sync) — rowMove is the only clean measurable.
- Anti-cheat floor: mean rowMove must be > STEP*0.75 so a frozen or
half-applying scroller (near-zero variance, would false-green) is caught.
- Old scrollTop-referenced residual kept as a printed NON-GATING diagnostic
(= estimate error R = T3's estimator acceptance number).
- Second non-gating diagnostic: one wheel-actuated pass of the same metric
(Tyler's real input is a wheel), on the record every run.
- RED-at-tip is a HARD gate with a VOID comment: median-of-run is only
valid while the corpus produces varying realization, and the tip run
being RED IS the proof of that dispersion.
RED at tip: peak-dev 41px / rms 24px (gate <=2 / <=0.6). GREEN target is
Dawn's real T2 writer: peak-dev 0.00 / rms 0.00, reproducible over 3 sync
runs. A synthetic per-notch-varying oracle drives 74/77 notches to exactly
STEP; the real writer re-pins in the same ResizeObserver cycle and closes
the synthetic-only outliers.
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Tyler's report: scrolling UP a fully-loaded channel is subtly jumpy;
DOWN is smooth. Root cause (Eva H1 + L-C): rows above the opening
viewport have never painted, so under content-visibility:auto they sit
at estimateRowHeight()'s contain-intrinsic-size reserve (60px,
utilities.css:17). Scrolling up, each realizes at its true height; the
reserve-vs-true delta shifts the reading position. Shipped WKWebView has
no overflow-anchor, so it corrects nothing — every delta lands raw.
This adds a deterministic gate that is RED at tip 77bd0e70 and that the
converged owned-compensation fix (overflow-anchor:none + same-frame
scrollBy(realized-reserved)) turns green:
- e2eBridge: a dedicated `jitter-corpus` mock channel (400 structurally
heterogeneous rows — headings/lists/blockquotes/code fences/long prose
whose true height estimateRowHeight is known to miss), its own channel
so the uniform deep-history seed the load-older specs use is untouched.
- upscroll-jitter.perf.ts: forces overflow-anchor:none on the scroller
(mirrors shipped WKWebView; logs CSS.supports per engine), then wheels
up in notches tracking a row held INSIDE the viewport (SAFE_MARGIN band
from both edges, re-picked each step, scored only if the same id stayed
in-band before+after). Residual = row's visual delta-top minus the
scroll delta: 0 == smooth. Gate is peak <=2px, rms <=0.6px.
Validity: within a single cold run the already-realized rows read
residual 0.0 exactly while realizing rows drift -27..-75px — an artifact
would drift on both. Negative sign shows the estimator over-reserves.
At tip: peak 75px, rms 40px over 23 steps → RED. A correct fill of
scrollTop absorbs the drift into the scroll delta, driving residual->0.
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>