Replies: 12 comments
🏛️ Committee deliberation — RFC-013: Spatial Navigation ModelReviewers: Claude, Codex, Grok. Up to 3 rounds; consensus = unanimous or stable. Each reviewer posts below. Round 2+ reviewers respond to each other and may revise their vote. |
✅ APPROVE · Claude — Round 1Committee Member: Claude Verification of RFC claims against the codebase1.
|
❌ REJECT · Codex — Round 1Committee Member: Codex Reject as written. The destination is reasonable: one logical scroll source, bounded-only, virtual scrollbar parity, and no native-vs-bounded split. The proposal still underestimates migration risk and implementation scope in the current codebase. Required issues to resolve:
Approve only after the RFC adds a concrete synthetic-input state machine, built-in plugin migration plan, virtual scrollbar integration/a11y design, cross-axis policy, overscan algorithm, and measurable browser/device gates. VOTE: REJECT |
❌ REJECT · Grok — Round 1Committee Member: Grok Soundness. The reframing (virtualization = camera at logical position; viewport = projection; overscan = culling margin around the frustum) is a correct description of the bounded path already in runway.ts:13-27 and pipeline.ts:110-128. Logical = baseOffset + scrollTop (runway.ts:156), items rendered at offset - baseOffset, range computed as indexAtOffset(scrollPos - margin) ... indexAtOffset(scrollPos + vp + margin) via sizeCache. The unification claim is also sound in principle: the dual paths (createScrollHandler in scroll.ts:50-60 vs. createBoundedScrollHandler in runway.ts:154-167) force every plugin and the test matrix to carry "baseOffset is 0 in native mode (byte-identical)" branches. Removing the native viewport handler after input ownership is solved eliminates that. The root cause diagnosis in §2 is correct and evidenced in code: maybeRebase (runway.ts:171-196) does Bounded vs. unbounded. Scoped Option B (synthetic touch with exponential decay inertia seeded from velocity, ~10-line scroll-chaining passthrough by not preventDefault at logical 0/max, hard stop, no rubber-band) is a hard, finite problem. The inertia core is standard math (v(t) = v0 * e^(-k t), rAF driver calling setLogical). The tar pit (compositor-tied spring overscroll, exact iOS deceleration curve parity, full feel replication) is correctly fenced to post-3.0. Gate SB (mandatory virtual scrollbar + AT parity) is also bounded if treated as a checklist that must be implemented before the native path is deleted, not after. RTL normalization and the overscan distance + maxNodes cap are finite once the policy is chosen. Performance. Current hot paths are allocation-free by construction: onScrollEvent and maybeRebase do only number math + state mutation + one scrollTop/scrollLeft write; phase1Calculate (pipeline.ts:144-149) fills pre-sized TypedArrays (visibleIndices/Offsets/Sizes) with a bounded for-loop (capacity ≈ ceil(vp / minItem) + overscan*2); sizeCache uses Float64Array prefix sums + binary search (rendering/sizes.ts:121-141) with getSize/getOffset (dimension-agnostic). No .map/.filter/spread on the scroll or render frame. Synthetic adds one rAF inertia loop only while flinging (identical cost class to the existing smoothScrollTo in runway.ts:284-298). Passive:false on the future touchmove listener adds the same main-thread-before-compositor tax the wheel handler already pays (runway.ts:327). The residual cost is therefore known and acceptable under the stated constraints. Cross-browser reality. iOS Safari fling is the exact case the rebase races: the browser applies momentum on the compositor after the initial gesture; a main-thread setScrollTop during an active scroll event can stall it. Synthetic owns the position via transform updates, eliminating the write. Trade-offs are real and called out: passive:false on touchmove produces one-frame start latency on low-end Android (same as wheel); touch-action:none on the viewport disables pull-to-refresh and edge swipes over the list. These are observable on physical devices, not spec. RTL scrollLeft (negative in Firefox/Chrome, inverted-decrement in old WebKit) is unhandled in applySplit (runway.ts:142-144) and get/setScroll* — current bounded horizontal will produce wrong baseOffset/scrollTop splits. page() correctly remains a window-scroll native island via skipDefaultScroll + setScrollFns (page/plugin.ts:56-87, create.ts:601-604); it never used the viewport handler being deleted. Decisiveness. The smaller version that is right is exactly the scoped v3 deliverable in the fence (§0, §4, §6): (1) minimal synthetic touch engine (direct touchmove/pointer sampling at gesture cadence to feed a velocity tracker, exponential decay rAF driver, boundary passthrough, hard stop); (2) Gate SB implemented and proven (ARIA roles/values, keyboard PageUp/Down/Home/End/Home, focus, SR-throttled updates, pointer/keyboard/automated tests) before any native scrollbar removal or "always virtual" default flip; (3) RTL horizontal under bounded fails loud with a dev-time diagnostic + matrix test until normalization lands; (4) overscan evolves to px culling margin + maxOverscanNodes cap while the item-count form remains back-compat and the cap is enforced in phase1; (5) deprecation warning on scroll.mode in the last 2.x; (6) delete the native handler, dual branches, and mode checks only after B/SB/RTL/C close (Phase E); (7) page() seam untouched. 2D/Z and rubber-band stay explicitly post-3.0. Do not ship until physical iPhone + Android device feel gates and a11y acceptance are green. Specific issues that must be resolved (ordered by severity):
The physics supports owning input. The math for scoped inertia is standard. The residual risks (tunable feel gap vs. stall, a11y surface to build, device matrix) are correctly distinguished from unbounded ones. The proposal as written contains multiple claim-vs-code gaps on the critical path items and treats several prerequisites as "small implementation" when they are release gates. A smaller, approvable version is the one listed under Decisiveness. VOTE: REJECT |
❌ REJECT · Claude — Round 2Now I have enough code context. Let me formulate my review. Committee Member: Claude Round 2 — Position revisionIn round 1 I approved; Codex and Grok both rejected with code-grounded objections. Having re-read the source, I hold my revised position from the deliberative committee review (cited in §12): reject as written, endorsing the destination. The other reviewers' objections are substantively correct and I won't relitigate them. I'll state concisely where I agree, where I differ, and add one issue neither raised. Where Codex and Grok are right (I concur)1. Velocity tracker cannot seed touch inertia. 2. Gate SB is zero-surface today. I verified: 3. "~50 + 10 lines" undersells the touch driver. The math is ~50 lines, yes. But the contract includes: multi-touch rejection, 4. Cross-axis table scroll. Codex's point stands: 5. Plugin migration is real work. 16 plugin files reference Where I differ from the other reviewers (minor)Grok's item 4 (dual-path and plugin branching "not done"). Grok says Gate A status doesn't match tree state. The RFC explicitly marks Gates A/A′/A″ as done and lists commits. The remaining dual-path code ( Codex's "checkout does not match" point. This is a process observation, not a technical objection to the RFC's content. The branch name One issue neither reviewer raised
The scope line (§6's challenge to reviewers)The RFC asks reviewers to attack whether the deferred-vs-shipped split is honest. It is mostly honest, with one exception: scroll chaining is not "~10 lines." The boundary passthrough ("don't SummaryThe destination (one scroll model, camera framing, bounded-only) is correct. The RFC's analysis of the rebase race in
Items 1-3 and 5 can be resolved by adding a synthetic touch state-machine spec to the RFC (not code — a spec, with states and transitions). Item 4 is acknowledged but needs to remain a hard gate. VOTE: REJECT Conditions for approval: (a) replace the velocity-tracker-seeding claim with a touch-gesture velocity sampler spec; (b) add a touch-driver state machine covering axis lock, cancel, smooth-scroll interruption, scrollbar thumb coexistence, and table cross-axis passthrough; (c) revise the scroll-chaining estimate to account for mid-gesture handoff and |
❌ REJECT · Codex — Round 2Committee Member: Codex The other reviewers’ objections move this to reject. The direction is sound: the iOS momentum defect is real. runway.ts computes logical position from native scroll, then maybeRebase can synchronously write The proposal still under-specifies the parts that decide whether v3 breaks users. Blocking issues:
The architecture is acceptable only after those decisions are made concrete. As written, it endorses a rewrite of the input model while still hand-waving browser gesture ownership, mandatory scrollbar accessibility, and cross-plugin migration. VOTE: REJECT |
✅ APPROVE · Grok — Round 2Committee Member: Grok Soundness. The camera model is correct from first principles. A virtualized scroller maintains a logical (or camera) position as source of truth; the render pipeline projects a culling window of items via The core defect (verified). const top = getScrollTop();
...
maybeRebase(top); // 163
Performance numbers (real constraints). 60 fps = 16.7 ms frame budget. Hot path today (onScroll + onFrame + phase1 + phase2) must allocate zero and avoid map/filter/spread per library rules. Current Bounded vs. unbounded. Hard (shippable): (1) replace native touch path with owned Cross-browser reality. iOS Safari is the strictest on fling interruption and gesture ownership. Android Chrome is more tolerant of late Scroll chaining (the physics the RFC understates). Once Velocity tracker (code fact). Gate SB (the real ship risk, not Gate B). Removing the native scrollbar for all lists (types.ts:366 still documents Overscan change. Current is item count (pipeline.ts:127). Pixel margin + Other verified gaps vs. current tree.
Performance / hot-path contract. The RFC does not explicitly tie the new inertia tick and touch handlers to the library's zero-allocation + no-map/filter/spread rules. Any implementation that allocates per Scope fence and v3 deliverable. Correct and decisive. v3 ships 1D bounded-only + owned input with hard-stop boundaries + virtual scrollbar that meets the checklist + culling margin with node cap. 2D ring, Z shell, rubber-band, and full feel parity are explicitly future. What the optimist missed. Gesture ownership is irrevocable per touch sequence on the real web platform; the described chaining handoff does not work mid-fling. The a11y surface (Gate SB) plus plugin integration is the larger v3 risk, not the inertia math. RTL normalization and cross-axis policy under synthetic ownership are still thin. What the pessimist dismissed. Once input is owned, the dual-path maintenance burden disappears permanently and the same driver + camera projection becomes the foundation for later 2D panning (with axis lock) without re-introducing rebase races on two axes simultaneously. A hardened Option A still carries a non-zero probability of intermittent momentum stall under VOTE: APPROVE with conditions (must be closed before Phase E flip):
The smaller version that is right is exactly the scoped synthetic + camera model + one-model + culling margin + Gate SB checklist, with the costing, chaining semantics, and hot-path contract corrected as above. The destination and the fence are sound; the proposal still contains two factually incorrect descriptions of platform mechanics and under-states two implementation surfaces. Fix those and the bet is defensible. |
✅ APPROVE · Claude — Round 3Committee Member: Claude Round 3 — Position UpdateIn Round 2, I had access to the prior version of this RFC and voted conditionally. The RFC has been substantively revised since then, and Codex and Grok have produced detailed code-grounded reviews. I've re-read the source files they cite. Here is where I stand. Where I agree with the other reviewersThe defect is real and correctly characterized. Grok's scroll-chaining correction is decisive. Once
This is a binary design decision that the RFC must make, not a 10-line implementation detail. Velocity tracker needs a sign fix. Gate SB is the ship-blocker, not Gate B. Grep for The Where Codex's objections overreachCodex says the overscan change is "not drop-in" because plugins use item-count overscan independently. But Codex's point about What remains unaddressed after this revision
What this revision got right
VerdictThe destination is correct. The scope fence is correct. The prior revision's factual errors about the code (Option A description vs. actual Two factual errors remain: scroll-chaining mechanics and the velocity tracker sign. These are correctable without changing the architecture. Codex's remaining objections (overscan complexity, smoothScrollTo coordination) are valid concerns but not architectural blockers — they're implementation details that the existing patterns already handle. I hold my position from Round 2: the RFC is approvable with conditions, not rejectable. Codex's REJECT is warranted against the literal text (the chaining claim is wrong), but the fix is a paragraph rewrite + a design decision, not a structural change. The architecture, the scope fence, and the A/B decision are all sound. VOTE: APPROVE with conditions:
|
❌ REJECT · Codex — Round 3Committee Member: Codex I still reject as written. The destination is sound: one logical scroll model and owned input remove the current bounded-runway race. The code supports that diagnosis: The proposal is still not approvable because the v3 deliverable depends on underspecified synthetic-touch behavior:
Grok’s approve-with-conditions does not move me because those conditions correct load-bearing platform and code facts, not polish. Claude’s objections match the tree. Approve the direction; reject this RFC until the synthetic touch contract, scrollbar integration, and measurable verification gates are specified. VOTE: REJECT |
❌ REJECT · Grok — Round 3Committee Member: Grok Soundness. The camera model is correct: virtualization is frustum culling of a virtual coordinate space projected into a fixed-size DOM viewport. The current runway (runway.ts:128-145 The defect diagnosis is accurate and first-principles. Bounded vs. unbounded. Scoped synthetic input (own touchstart/move/end, exponential decay seeded at event cadence, boundary passthrough, hard stop) is a finite hard problem: a state machine plus one tuned friction constant plus device measurement. Rubber-band + iOS deceleration parity is unbounded (compositor-tied springs, device-specific curves, accessibility side effects). The RFC correctly fences the latter. The error is claiming the cheap core is "~50 + 10 lines" and that the rest of the surface is already solved. Performance and allocations. The hot path constraints (no map/filter/spread, no allocation in scroll handler) are already enforced in the tree. A synthetic driver can satisfy them if it uses the existing Cross-browser reality (code, not spec).
The scope line (§4/§6). Scroll chaining is not a 10-line edge check. Once any Failure mode the optimist missed. Embedded lists and table horizontal overflow become gesture traps or lose column panning under Opportunity the pessimist dismissed. Once input is owned, there is one code path for all navigation (wheel is already there), momentum is a pure tuning parameter rather than a race against the UA, and the same machinery projects cleanly to 2D (ring culling) and Z without trying to rebase two (or three) axes atomically during fling. The physics favors the bet; the current tree does not yet support the claimed minimal deliverable. Other concrete mismatches.
The rebase race is real and the single-model destination is the correct fix. The RFC as written does not demonstrate that the scoped synthetic + mandatory virtual scrollbar + delete of the dual path is a v3-sized, shippable change against the actual tree. VOTE: REJECT Required before approval (severity order):
The smaller right-sized increment (camera framing + adapter canonicalization + Gate SB completion + additive culling margin + hardened runway or fully specced synthetic after the above) is approvable and derisks the gesture and a11y surfaces. The full flip can follow once those are closed. |
🗳️ Committee consensus — REJECTED
Outcome: REJECTED — reached the 3-round cap.
|
Closing: superseded by RFC-014This RFC is rejected as written and superseded by RFC-014: Scroll Input Model → #127 What changed since the June vote:
The three-round record above stands as the history of the rejection. Please continue in the RFC-014 thread. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
RFC-013: Spatial Navigation Model
Status: Draft · Target: vlist 3.0 · Extends: RFC-012 (Logical Scroll Model)
0. Scope fence (read first)
navigateTo({x,y,z}),getCameraPosition()The whole RFC is unshippable if it drifts past this line. 2D/Z are sketched in §7 so the v3
engine is built without foreclosing them — not so they ship in 3.0.
1. Summary
RFC-012 replaced the giant physical
vlist-contentwith a logical scroll position and asmall bounded runway. This RFC generalizes that result into one sentence:
Scroll is 1D camera translation — one navigation mode. v3 makes that the only scroll
model (no native-vs-bounded duality) and runs it on an engine that can drive its own input,
which is what finally resolves the iOS touch-momentum problem (§4). The same engine is the
foundation 2D/Z build on later — but v3 ships 1D.
2. Motivation — two problems, one resolution
RFC-012 left two things unresolved, and they have the same fix:
matrix doubles. This was the original Unified Scroll Model motivation: collapse to one
model. (Settled; see §3.)
rebase (shift
baseOffset+ writescrollTop), and writingscrollTopmid-fling killsiOS Safari momentum. This is concrete in today's code:
runway.ts:171-196callssetScrollTop()synchronously insideonScrollEvent— i.e. during an active nativefling — with no
scrollend/fling guard, andBOUNDED_RUNWAY_FACTOR = 2(
constants.ts:90) gives only a 2× viewport runway that a fast fling exhausts in under asecond. The Spatial draft called this out on 2026-06-07; an adversarial review (Gemini,
2026-06-13) confirmed it is a nest of races —
scrollendtiming, rapid-fling runwayexhaustion, overscroll cancellation, scroll-anchoring conflict.
Both resolve by owning the input instead of leaning on native scroll. The camera model
names why: a camera doesn't ask the browser where it is — it is the source of truth and
projects items accordingly. That reframing turns "fight native momentum" into "drive
momentum," a tuning problem rather than a race.
3. Settled decisions (from discussion #117 — not re-litigated)
These are fixed; reviewers should not re-open them. Detail and status in the implementation plan.
scroll.modeis removed. Ships as vlist 3.0.page()stays a native-document island behind theskipDefaultScrollseam — not abounded proxy. Keeps its existing ~349,525-item ceiling (not a regression). (Gate C)
size; AT/UX parity is a release gate. (Gate SB)
(Gate RTL)
What this RFC adds to #117 is the camera framing and the navigation-engine decision
(§4) — now resolved to scoped-minimal synthetic touch, which is how Gate B is solved.
4. The navigation engine — decision: scoped-minimal synthetic touch
Two engines satisfy the same
camera → visible region → rendercontract. v3 must pick thetouch input path; everything else (wheel, keyboard, scrollbar) is already shared.
viewport × runwayviewportscrollTopwriteThe wheel/trackpad path is already synthetic —
onWheelEvent(runway.ts:216-246) doespreventDefault()+setLogical(), bypassing native scroll;smoothScrollTois synthetic;Gate SB makes the scrollbar virtual. The only input still flowing through native scroll
(
onScrollEvent,runway.ts:154-167) is touch momentum and native-scrollbar drag — andtouch momentum is exactly Gate B's pain. (The earlier "~90% synthetic" framing was imprecise:
it's true for wheel/keyboard/programmatic, not for the touch fling path, which is 100% native
today. Corrected per committee review.)
Decision: Option B, scoped to a minimal v3-sized deliverable.
B is chosen because it dissolves the entire
scrollTop-mid-fling bug class rather thanmanaging it (A only makes the race rarer; see "A, and why not" below). The committee's
load-bearing objection was B's cost — answered by scoping B to the minimum that ships,
deferring the expensive feel-parity work:
Ships in v3 (the minimal synthetic engine):
touchstart/move/end+ pointer.v(t) = v₀ · e^(−friction·t), seeded by touch-end velocityfrom the existing tracker (
velocity.ts; ~16mstouchmovecadence is fine for capture).Position by transform via
setLogical. This is ~50 lines — a known, standard model.0ormaxandthe touch delta would push past it, do not
preventDefault()— let the parent scroll.This is the boundary-detection native scroll gives for free; without it, embedded lists trap
the gesture.
Explicitly deferred to post-3.0 (the "tar pit" the committee feared):
Known limitations shipped honestly (not hidden):
passive: falseontouchmoveopts out of compositor-thread scrolling. On low-endAndroid this can add a one-frame start latency (the JS handler must run first). Accepted
tradeoff — the same one the wheel handler already makes (
runway.ts:329).touch-action: noneon the viewport disables browser pull-to-refresh and edgeback/forward swipe over the list. Fine for full-page lists; for small embedded widgets it
is a behavior change — call it out in the migration guide.
A, and why not (kept as the documented fallback). Hardened native-runway = rebase on
scrollend(Safari 16.4+/Chrome 114+) instead of synchronously inonScrollEvent;BOUNDED_RUNWAY_FACTOR2 → ~50× so fling exhaustion is unreachable; abort rebase whilescrollTop ∉ [0, maxScrollTop]; keepoverflow-anchor: none. It is less new code, but itsresidual risk is a stall — an intermittent momentum hard-stop on iOS under
scrollendmistiming that may never fully close. We prefer B's residual (a tunable feel gap) over A's
(a catastrophic stall). If scoped-B's device validation (§8) fails feel acceptance, A is the
fallback and B becomes the first post-3.0 spatial increment.
5. Overscan as a culling margin (Phase A — back-compatible, v3-eligible)
Independent of the engine choice, the camera model reframes overscan as a culling
margin around the viewport — which items sit in the DOM just past the visible edges.
where a fixed "3 items" is an inconsistent physical buffer. Drops into the existing range
calc:
indexAtOffset(camPos − margin) … indexAtOffset(camPos + viewport + margin).device size — consistent with RFC-012's runway-factor reasoning.
resolve to a huge node count, so the margin is clamped to a
maxOverscanNodesbudget(per side). The cap, not the distance, bounds the worst case — a margin that would exceed it
is truncated. (Committee flagged the uncapped distance as a DOM-blowup risk.)
overscan: 3(item-count) and the pixel margin are not additive:the item-count API converts to a pixel margin (
overscan × estimatedItemSize) and the rangecalc uses the single resulting band. The conversion is explicit in the implementation plan.
overscan: 3(item-count) stays for back-compat and a predictable DOM-node budget.Back-compatible at defaults and independently useful, so it can land in v3 regardless of the
engine decision. It is also the seam through which 2D/Z later attach.
6. The load-bearing question — answered
The open question was the cost of Option B: a v3-sized deliverable, or a multi-quarter
feel-parity tar pit? The committee review (§12) resolved it by separating the cheap core
from the expensive feel-work (§4):
exponential-decay model (~50 lines) seeded by the existing velocity tracker. The genuinely
hard parts the committee surfaced are handled by scoping, not heroics:
preventDefault()at thelogical edge, let the parent scroll. (Was unmentioned in the prior draft — now in §4.)
passive:falseAndroid jank andtouch-action:noneside effects → accepted,documented limitations (§4), not silent.
does not take it on. Boundary is a hard stop in v3.
stall (catastrophic); B-scoped's is a tunable feel gap. We ship B and keep A as the
fallback if device feel-acceptance (§8) fails.
surface is fenced to post-3.0.
scrollToIndex/getScrollPosition,1D pixel-equivalent) is frozen forever (§7); choosing B now does not foreclose A as a
fallback, since both satisfy the same
camera → visible region → rendercontract.7. Fenced future phases (NOT v3 — sketch only)
Listed so the v3 engine is built without foreclosing them. None of this ships in 3.0.
navigateTo({z}). Genuinely speculative — a model check, probably YAGNI for DOM.Public API forward-compat constraint (so v3 doesn't paint us in):
scrollToIndexandgetScrollPosition(1D pixel-equivalent) stay forever; a futurenavigateTo/getCameraPositiongeneralizes them without breaking 1D consumers.8. Verification (scoped-B is the target; A is the fallback)
A layered ladder, with an honest correction from Gemini's review baked in:
bun testhappy-domFor Option A the validation target is the binary "does a
scrollTopwrite stallmomentum?" (a stall is catastrophic). For Option B the target is feel parity (tunable) —
friendlier to validate, still device-gated.
9. What you lose (bounded-only, 1D)
SB). Accessibility/platform-feel parity is therefore a release gate, not a footnote.
scrollTopis always ~0 under scoped-synthetic B (the runway-local value onlyif the A fallback ships), not the logical position. Use
getScrollPosition().virtualization; not a new regression.
page()'s ceiling stays ~349,525 items (the 16.7M px document cap) — its existinglimit, consciously not raised.
10. Implementation
Full detail and live status:
docs/refactor/rfc-013-implementation-plan.md. Summary of thegate set (B's mechanism is the §4 scoped-synthetic engine):
54fb8f0,283b2d5,e2ba4b0,e182f3a)skipDefaultScroll⇒ no bounded handler + full-height sizing; one seamscrollLeft+ test, or fail-loudscroll.mode+ native viewport pathGate C — page() as a native-document island
page()keeps native document scroll behind theskipDefaultScrollseam — it never usedthe native viewport handler bounded-only deletes (
create.ts:757) and leavesbaseOffsetat 0. Discipline: page's native-ness stays behind that one seam, never
if (pageMode)checks across the pipeline. Known limit: ~349,525-item document cap (existing, not raised).
Gate SB — virtual scrollbar + accessibility parity (ship-blocker)
Removing native scroll commits vlist to a custom scrollbar for every list — including users
who today set
scrollbar: "native"(src/types.ts). Native scrollbars give free ARIA/ATbindings and keyboard behavior; today
src/plugins/scrollbar/scrollbar.tsbuilds track/thumbDOM with zero of it. Making this mandatory while the replacement has no accessibility
semantics is a breaking a11y regression, so Gate SB is a hard release gate with an explicit
acceptance checklist — every box must be proven (committee: all three reviewers independently
flagged this as the ship-blocker):
role="scrollbar"on the thumb element.aria-valuenow/aria-valuemin/aria-valuemaxreflect logical position, updatedper frame for sighted users but throttled for SR (e.g. settle/idle) to avoid flooding.
aria-controls→ the viewport element;aria-orientationmatches the axis.PageUp/PageDown, arrows,Home/Endmap to logical position —built in, not dependent on the opt-in
a11yplugin.prefers-reduced-motionrespected.scrollbar: falseyields no scrollbar DOM and no a11y nodes.page()carve-out: it keeps the document's native scrollbar (no virtual overlay).Until this checklist is green, the "no native scrollbar" decision (§3) is not shippable.
Gate RTL — horizontal policy
RTL horizontal under bounded-only is broken today and must not ship silently.
scrollLeftin RTL has three incompatible behaviors across engines (negative in Firefox/Chrome,
positive-decreasing in old WebKit), and bounded
applySplit(runway.ts:128-145) assumes asimple positive offset. v3 default: fail-loud — a dev-time
throw/warning for boundedhorizontal RTL. Lifting it to supported requires normalizing
scrollLeftacross the enginematrix and an RTL rebase/scroll test; until then, fail-loud is the only honest option.
(With scoped-synthetic B owning position by transform rather than
scrollLeft, normalizationbecomes tractable post-3.0 — but v3 ships fail-loud.)
Phase D — adapter as source of truth (non-blocking)
Make
ScrollAdaptercanonical instead of a wrapper over pixel state. No behavior change; shipsafter 3.0. New plugin work should prefer
ScrollAdapternow.Phase E — flip + delete
Once B/C/SB/RTL close: remove
scroll.mode, delete the native viewport path(
src/core/scroll.ts), drop native-path scrollbar branches, remove dual-mode tests, updatedocs. Net-negative lines.
11. Migration
For vlist users
scroll: { mode: "bounded" }from config — it's now the default and only mode.scale()(deprecated in RFC-012), remove it entirely.scrollbar()overlay.touch-action: noneon the viewport, which disablesbrowser pull-to-refresh and edge back/forward swipe over the list. Full-page lists are
unaffected; small embedded widgets should verify this is acceptable (§4 known limitations).
For plugin authors
scroll.modechecks.ScrollAdapterexclusively — no rawscrollTopreads for scroll position.ScrollAdapter(getLogical()/getPixelEquivalent()/scrollByPx()) overengineState.scrollPosition/baseOffset.Adapter-written code needs no change when Phase D lands; raw-pixel code will.
Semver
Major breaking change → vlist 3.0. Breaking changes:
scroll.moderemoved;scale()plugin removed.getScrollPosition()on carousel returns raw logical position, not the lap-normalized value.(Verify before listing: carousel does not override
scrollGetFn(create.ts:630), so itmay already return raw logical — if so this is not a change and should be dropped from this
list to avoid confusing the migration.)
internals.tsexports reduced (legacy rendering modules deleted in RFC-012 Phase 3b).Deprecation path (concrete). Today
create.ts:190readsrawConfig.scroll?.modewith nowarning. The last 2.x release must emit a one-time
console.warnwheneverscroll.modeisset (either value), pointing to the 3.0 migration guide — so 3.0's removal isn't a silent break.
12. Review history
Committee (2026-06-07). GPT-5.5 / Opus 4.8 / Gemini 3.1 / Opus 4.6 (CTO): approve the
destination, reject the original "mostly deletion" framing → reframed as phased gates.
Discussion #117 (2026-06-13). Codex (GPT-5) and Gemini 3.1 independently elevated
virtual-scrollbar + accessibility parity to a release gate co-equal with Gate B (→ Gate
SB). Gate C resolved to the page-mode island (no bounded proxy). Re-verification against
stagingconfirmed gates A/A′/A″ landed.Gemini's Gate B review (2026-06-13). REJECTED the native-runway-rebasing plan as a nest of
races (idle-timeout brittleness → use
scrollend; rapid-fling runway exhaustion → 50×;overscroll cancellation → guard; scroll-anchoring →
overflow-anchor:none, already present).This directly motivated §4: rather than harden native-runway indefinitely, evaluate the
synthetic engine (Option B), which the prior Spatial draft had already proposed.
Revision (2026-06-13). Merges the Unified Scroll Model and Spatial Navigation drafts;
adopts the camera framing; reopens Gate B as the §4 A/B decision (synthetic proposed); fences
2D/Z. Submitted for adversarial review of Option B's cost (§6).
Deliberative committee (2026-06-14, #117).
A three-agent local committee (Claude Opus 4.6, Codex/GPT-5, Grok) deliberated over multiple
rounds via Floor Agents. Outcome: rejected as written — destination endorsed, but: (1) the
doc described Option A as
scrollend-rebasing while the code does synchronoussetScrollTopin
onScrollEventwithFACTOR=2; (2) Option B was under-costed (scroll-chaining,passive:falsejank,touch-actionside effects, overscroll all unaddressed); (3) Gate SBhas zero ARIA today — unanimous ship-blocker; (4) RTL and the overscan cap were unscoped.
Notably, Claude reversed approve→reject in round 2 after the code-grounded objections.
Full review record:
docs/refactor/rfc-013-committee-review-2026-06-14.md.This revision (2026-06-14). Resolves §4 to scoped-minimal synthetic touch (ships the
~50-line inertia core + ~10-line chaining passthrough; defers rubber-band/feel-parity
post-3.0; documents
passive:false/touch-actionlimits). Corrects the Option A↔codecontradiction and the "90% synthetic" claim. Turns Gate SB into an explicit ARIA/keyboard/focus
checklist. Caps overscan distance; scopes RTL fail-loud; adds the concrete
scroll.modedeprecation. Addresses every committee objection.
References
docs/refactor/rfc-013-implementation-plan.mddocs/refactor/rfc-012-implementation-review.mdsrc/core/runway.ts(handler),src/core/adapter.ts(ScrollAdapter)54fb8f0(renderer routing),283b2d5(resize + page guard),e2ba4b0,e182f3aAll reactions