Context
Operator L4 re-test on dev tip (post #12908 merge) found devindex "fully broken": cell content rendering under wrong headers (his screenshot: identical dates under four distinct headers; numbers under text headers) and header movements breaking, especially across heterogeneous widths. Headless reproduction tonight isolated the mechanism with a forensic probe.
Release classification: ON the board — this IS the v13 gate failure (parent #12883).
The Problem (reproduced + convicted)
Probe sequence: clean drags audit green; overdrag + drop in the scrolled state then breaks the world:
- Post-drop forensics: headers render at HOME positions (
Total@387, Commits %@487, …) while the first-row cells beneath are year-block cells (y2024, y2021, y2019 — 80px cells under 90-200px headers). Header row vs body offset by the FULL drop-time scroll (~1878px).
- The desync self-heals on the next manual scroll (the
GridHorizontalScrollSync addon copies scrollbar→header on the scroll event) — which is why post-scroll audits read clean while the post-drop window looks destroyed.
- Compounding failure: a second drag started inside the desynced window has the zone's scroll term (fed from the worker pipeline: ~1878) contradicting the toolbar element's actual scroll (0) — the drag-start
itemRects snapshot is taken in toolbar space, so every delta is off by the scroll amount: switch storms, wrong landings, and switchItems' columnPositions x/width writes land wrong — the persistent "columns duplicate / widths broken" corruption the operator accumulated interactively.
The Mechanism
At drag end, the items' wrapperStyle restore removes position: absolute. During the reflow there is a frame where the center toolbar has no scrollable overflow → the browser clamps the toolbar element's native scrollLeft to 0. Nothing re-applies it: the scrollbar element and the body's --grid-scroll-left var still hold the drop-time value; the addon only copies on scrollbar scroll events, and a same-value scrollTo on the scrollbar fires no event. The toolbar is left as the odd surface out.
The Fix
In draggable.grid.header.toolbar.SortZone#processDragEnd, after the restore/update chain settles (both branches — within-region landing AND lock-change re-home): re-apply the worker-side scroll truth to the toolbar element via Neo.main.DomAccess.scrollTo({direction: 'left', id: owner.id, value: owner.scrollLeft}) when owner.scrollLeft > 0. The body var and scrollbar are already correct; the toolbar rejoins them. Idempotent, scoped to the one surface that clamps.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback / Edge Case |
Docs |
Evidence |
grid header SortZone#processDragEnd (drag-end scroll re-sync) |
This conviction (probe forensics) + the #12908 pipeline model (scrollbar = SSOT; toolbar = display copy) |
After drop in a scrolled state, the header toolbar's native scrollLeft is re-applied from owner.scrollLeft (the worker mirror fed by grid.ScrollManager) |
owner.scrollLeft === 0 → no-op (unscrolled drops unaffected); lock-change branch re-applies after onColumnLockChange settles |
JSDoc on the re-sync block |
L3 headless: post-drop forensics show header↔cell alignment WITHOUT any subsequent scroll (new regression leg in GridColumnOverdragScroll.spec.mjs); L4: operator re-pass on #12883 |
| Second-drag-after-scrolled-drop math |
Zone scroll-term semantics (absolute, seeded from owner.scrollLeft) |
With the toolbar re-synced, the next drag's itemRects snapshot space and term agree |
Covered by the same re-sync; no separate code path |
n/a |
Probe: post-fix drag-after-scrolled-drop audits clean |
Acceptance Criteria
Out of Scope
- The deeper architectural question the operator raised (multi-body vs the main-branch single-body model) — if the re-sync + re-pass still leaves width-class failures, that conversation escalates to a Discussion.
Related
- Parent: #12883 · Pipeline model:
#12908 (#12906/#12907) · Mid-drag mapping: #12894
Live latest-open sweep: checked at 2026-06-11T23:1xZ; no equivalent. A2A in-flight sweep: clean.
Origin Session ID: e605ce21-3668-445c-bc00-45896aa9a092
Retrieval Hint: "scrolled drop header toolbar scrollLeft clamp desync body var drag-end reflow"
Context
Operator L4 re-test on dev tip (post
#12908merge) found devindex "fully broken": cell content rendering under wrong headers (his screenshot: identical dates under four distinct headers; numbers under text headers) and header movements breaking, especially across heterogeneous widths. Headless reproduction tonight isolated the mechanism with a forensic probe.Release classification:ON the board — this IS the v13 gate failure (parent #12883).The Problem (reproduced + convicted)
Probe sequence: clean drags audit green; overdrag + drop in the scrolled state then breaks the world:
Total@387,Commits %@487, …) while the first-row cells beneath are year-block cells (y2024,y2021,y2019— 80px cells under 90-200px headers). Header row vs body offset by the FULL drop-time scroll (~1878px).GridHorizontalScrollSyncaddon copies scrollbar→header on the scroll event) — which is why post-scroll audits read clean while the post-drop window looks destroyed.itemRectssnapshot is taken in toolbar space, so every delta is off by the scroll amount: switch storms, wrong landings, andswitchItems' columnPositions x/width writes land wrong — the persistent "columns duplicate / widths broken" corruption the operator accumulated interactively.The Mechanism
At drag end, the items'
wrapperStylerestore removesposition: absolute. During the reflow there is a frame where the center toolbar has no scrollable overflow → the browser clamps the toolbar element's nativescrollLeftto 0. Nothing re-applies it: the scrollbar element and the body's--grid-scroll-leftvar still hold the drop-time value; the addon only copies on scrollbarscrollevents, and a same-valuescrollToon the scrollbar fires no event. The toolbar is left as the odd surface out.The Fix
In
draggable.grid.header.toolbar.SortZone#processDragEnd, after the restore/update chain settles (both branches — within-region landing AND lock-change re-home): re-apply the worker-side scroll truth to the toolbar element viaNeo.main.DomAccess.scrollTo({direction: 'left', id: owner.id, value: owner.scrollLeft})whenowner.scrollLeft > 0. The body var and scrollbar are already correct; the toolbar rejoins them. Idempotent, scoped to the one surface that clamps.Contract Ledger Matrix
grid header SortZone#processDragEnd(drag-end scroll re-sync)#12908pipeline model (scrollbar = SSOT; toolbar = display copy)owner.scrollLeft(the worker mirror fed bygrid.ScrollManager)owner.scrollLeft === 0→ no-op (unscrolled drops unaffected); lock-change branch re-applies afteronColumnLockChangesettlesGridColumnOverdragScroll.spec.mjs); L4: operator re-pass on #12883owner.scrollLeft)Acceptance Criteria
Out of Scope
Related
#12908(#12906/#12907) · Mid-drag mapping:#12894Live latest-open sweep: checked at 2026-06-11T23:1xZ; no equivalent. A2A in-flight sweep: clean.
Origin Session ID: e605ce21-3668-445c-bc00-45896aa9a092
Retrieval Hint: "scrolled drop header toolbar scrollLeft clamp desync body var drag-end reflow"