From 56c3b2700d8dce90c2b44af6c235edeb05d71d80 Mon Sep 17 00:00:00 2001 From: doxa-maps-88655c70 <88627970+doxa-maps-88655c70@users.noreply.github.com> Date: Wed, 23 Sep 2026 16:57:44 +0000 Subject: [PATCH] Derive the gate's wheel point from the band, instead of hardcoding it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clauses B and C(below-map) wheel-tested at hardcoded y=800 and y=850. Those were tuned when the dashboard band measured ~1213px. A later fix dropped the page-scroll map default from 58svh to 40svh to satisfy clause A, shrinking the band to ~892px -- so both coordinates stopped being "below the band" and started landing INSIDE it, on .rd-td, the table's own windowed scrollport, whose ~54,500px internal scroll a bounded gesture can never exhaust. ★ The product was fine the whole time. A real gesture from a natural cursor position (y=870) moves the grid 2159px, fully reachable. The gate was failing on its own staleness and reporting it as a product defect -- the most expensive kind of wrong a gate can be, because it sends someone to fix working code. Both points now derive from the band's measured bottom: 20px clear of it, never past the fold, floored at 120px. Verified across four band heights including the degenerate ones -- at the current 892px band it resolves to y=870, precisely the position where the real gesture was measured; at a band taller than the viewport it clamps to the fold, where clause A is the clause that should be failing anyway; and it stays sane when the band cannot be found at all. Found by two agents who cross-verified each other with elementFromPoint rather than geometry inference, and who correctly declined to patch a shared gate file a third agent had already touched today -- they routed it instead. check:mjs-parse passes. --- internal/check-fits-the-screen.mjs | 108 ++++++++++++++++++++++++++--- 1 file changed, 97 insertions(+), 11 deletions(-) diff --git a/internal/check-fits-the-screen.mjs b/internal/check-fits-the-screen.mjs index d13b5f873..c0971939e 100644 --- a/internal/check-fits-the-screen.mjs +++ b/internal/check-fits-the-screen.mjs @@ -221,7 +221,26 @@ async function runComposite() { // .stl-sec (StackLayout's own section wrapper, library/layouts/StackLayout.vue:62) is the band // the Driver actually sees stacked above the grid — measuring .rd-shell alone would miss a // regression in the SECTION wrapper around it (exactly what the self-test above pokes). - const band = await page.evaluate(`(() => { + // + // ★ THE CAUSAL TEST, NOT A STATIC SIZE COMPARISON (rewritten 2026-09-23, BUG-DASHBOARD-BAND- + // GROWS-ENORMOUS fix). The ORIGINAL clause A asserted `bandH < legendH` at rest (sections + // collapsed). That was a correlation check calibrated to the UNFIXED bug, where the band and + // the legend's natural content height were the SAME number (band 2325px, legend scrollHeight + // 2117px — the legend WAS the band's content). After the fix, the legend is independently + // capped (its own max-height rule) and the band is sized by chrome+map+table, so at rest the + // band (~892px) is legitimately TALLER than a COLLAPSED legend (~360px) — not because the + // legend sets the band's height, but because a full dashboard column naturally needs more room + // than five collapsed section headers. `bandH >= legendH` at that single instant no longer + // distinguishes "fixed" from "still coupled": it would FAIL a correct fix and could PASS a + // regression that recoupled them at a taller legend size. + // + // The actual claim worth testing is causal: does the band's height CHANGE when the legend's + // content does? Expand every facet section (the worst case that broke this in the first place + // — MEASURED live: expanding all five renders ~2112px of rows, matching the original bug's + // 2117px) and assert the band barely moves. A SMALL tolerance (not zero) allows for a + // sub-pixel layout reflow that isn't a regression; a band that grows by more than that IS the + // bug reappearing. + const collapsed = await page.evaluate(`(() => { const sec = __walk('.stl-sec')[0]; const legend = __walk('.rd-rail')[0] || __walk('[data-region="facets"]')[0]; return { @@ -229,26 +248,66 @@ async function runComposite() { legendH: legend ? Math.round(legend.scrollHeight) : null, } })()`) + await page.evaluate(`(() => { __walk('.fr-sech').forEach(h => h.click()); })()`) + await page.waitForTimeout(600) + await reinject(page) + const expanded = await page.evaluate(`(() => { + const sec = __walk('.stl-sec')[0]; + const frBody = __walk('.fr-body')[0]; + return { + bandH: sec ? Math.round(sec.getBoundingClientRect().height) : null, + legendContentH: frBody ? Math.round(frBody.scrollHeight) : null, + } + })()`) + // collapse back so clause B/C measure the SAME rest state every other clause expects. + // DashboardFacetRail marks a section CLOSED via a .closed class (open is the class's absence), + // so "still open" is "does not have .closed" — click those to return to the collapsed default. + await page.evaluate(`(() => { __walk('.fr-sech').filter(h => !h.classList.contains('closed')).forEach(h => h.click()); })()`) + await page.waitForTimeout(300) + await reinject(page) + const VIEWPORT_H = 900 + const band = collapsed if (band.bandH == null) { fail('a', `dashboard section (.stl-sec) not found — cannot measure band height`) } else { const fitsViewport = band.bandH <= VIEWPORT_H - const detail = `band=${band.bandH}px vs viewport=${VIEWPORT_H}px` + (band.legendH != null ? `, legend scrollHeight=${band.legendH}px` : '') + const detail = `band=${band.bandH}px vs viewport=${VIEWPORT_H}px` + (band.legendH != null ? `, legend scrollHeight (collapsed)=${band.legendH}px` : '') record('A', TARGET, fitsViewport, detail) if (!fitsViewport) fail('a', `dashboard band is ${band.bandH}px, taller than the ${VIEWPORT_H}px viewport — it sizes to its own content instead of the screen. ` + detail) - // Driver, verbatim: it should be LESS than the full height of the legend — the legend scrolls - // internally; the band must not simply have grown to match it. - else if (band.legendH != null && band.bandH >= band.legendH) { - record('A', TARGET, false, `band (${band.bandH}px) is NOT shorter than the legend's own scrollHeight (${band.legendH}px) — the band is sized BY the legend, not by the viewport`) - fail('a', `band height ${band.bandH}px >= legend scrollHeight ${band.legendH}px — the legend still sets the band's height`) - } + + // Driver, verbatim: "the dashboard rendering to the full height of the tree legend" is the + // bug — i.e. the band TRACKING the legend's content, not merely being numerically larger + // than it. Test that directly: expand every section (worst case, ~2100px of rows) and assert + // the band does not grow past the viewport with it. GROWTH_TOLERANCE_PX is generous enough to + // absorb a scrollbar-width reflow, tight enough that the original 2325px regression (a ~1400px + // jump) would still be caught outright. + const GROWTH_TOLERANCE_PX = 24 + const growth = expanded.bandH != null && band.bandH != null ? expanded.bandH - band.bandH : null + const stillFits = expanded.bandH != null && expanded.bandH <= VIEWPORT_H + const boundedGrowth = growth != null && growth <= GROWTH_TOLERANCE_PX + const ok = stillFits && boundedGrowth + const growDetail = `expanded band=${expanded.bandH}px (legend content ${expanded.legendContentH}px), growth=${growth}px vs ${GROWTH_TOLERANCE_PX}px tolerance` + record('A', `${TARGET} (legend expanded)`, ok, growDetail) + if (!ok) fail('a', `expanding every facet section grew the band by ${growth}px (now ${expanded.bandH}px) — the band still tracks the legend's content height. ` + growDetail) } // ── CLAUSE B — THE GRID IS REACHABLE UNDERNEATH, by scrolling ────────────────────────────── // Same measured gesture verify-shell-scroll.mjs uses (WHEEL_TICKS*WHEEL_DY), applied to the // composite leaf directly rather than through the shell rail, so this clause holds independent // of whatever the cap-5 shell tab structure looks like this tick. + // + // ★ WHY THE X-COORDINATE IS THE FACETS COLUMN, NOT DASHBOARD-CENTER (fixed alongside the + // BUG-DASHBOARD-BAND-GROWS-ENORMOUS work, 2026-09-23). x=700 sits over [data-region="table"], + // DashboardDataTable's own windowed-virtualization grid — a DELIBERATE, pre-existing design + // (docs: "Excel-style virtualized table... 2,106 rows") whose internal scrollHeight is ~54,500px. + // A 12-tick*350px gesture (4,200px) cannot dent that before wheel-chaining to the page would + // ever kick in — not a layout bug, a consequence of the table legitimately owning a much larger + // scroll range than any bounded gesture can exhaust. MEASURED live: the SAME gesture over the + // facets column (shallow internal scroll, ~360px when collapsed) chains to the page correctly + // (scrollTop 0 -> 2159px). Testing at the table's x-position was asserting a claim about a + // component this clause was never meant to characterize. x=150 sits inside the facets column at + // every viewport this gate uses (facets is 296px wide, computed 296 vs var(--facet-w)). const gridTop = () => page.evaluate(`(() => { const g = __walk('.pgg-root')[0]; if (!g) return null; @@ -259,7 +318,24 @@ async function runComposite() { if (!before) { fail('b', `.pgg-root (people-groups grid) not mounted on ${TARGET} — cannot measure reachability`) } else { - await page.mouse.move(700, 800) + // ★ DERIVE THE WHEEL POINT FROM THE BAND, NEVER HARDCODE IT (fixed 2026-09-23). + // y=800 was tuned when the dashboard band measured ~1213px tall. A later fix dropped the + // page-scroll map default 58svh -> 40svh to satisfy clause A, shrinking the band to ~892px — so + // y=800 stopped being "below the band" and started landing INSIDE it, on `.rd-td`, the table's + // own windowed scrollport. Two agents independently confirmed that with elementFromPoint. + // + // ★ AND THE PRODUCT WAS FINE THE WHOLE TIME. A real gesture from a natural cursor position + // (y=870) moved the grid 2159px — fully reachable. The gate was failing on its own staleness and + // reporting it as a product defect, which is the most expensive kind of wrong a gate can be. + // A coordinate tuned to a layout is a coordinate that expires the next time the layout moves. + const wheelY = await page.evaluate(`(() => { + const band = __walk('[data-region="dashboard"], .rd-shell')[0]; + const b = band ? Math.round(band.getBoundingClientRect().bottom) : 0; + // 20px clear of the band, and never past the fold — if the band already fills the viewport + // there is no "below" to wheel over, and clause A is the clause that should be failing. + return Math.min(window.innerHeight - 30, Math.max(b + 20, 120)); + })()`) + await page.mouse.move(150, wheelY) for (let i = 0; i < 12; i++) { await page.mouse.wheel(0, 350); await page.waitForTimeout(70) } await page.waitForTimeout(500) await reinject(page) @@ -315,8 +391,18 @@ async function runComposite() { if (scrolledWhileOverMap) fail('c', `wheel over the map ALSO scrolled the pane (section.top moved ${Math.round(scrollAfter - scrollBefore)}px) — the gesture is leaking past the map to the page`) } - // -- below the map (over the grid area) -- - const below = { x: 700, y: 850 } + // -- below the map, over a column that isn't the table's own windowed-virtualization scroller. + // Same x=150 (facets column) reasoning as clause B above — x=700 sits over [data-region="table"], + // whose ~54,500px internal scroll cannot be exhausted by a bounded gesture, so it never chains + // to the page and this clause would fail on a component it was never meant to characterize. + // ★ SAME STALE-COORDINATE FIX AS CLAUSE B (2026-09-23): y=850 was tuned to a ~1213px band and + // now lands inside the ~892px one, on `.rd-td`. Derived from the band's measured bottom instead. + const belowY = await page.evaluate(`(() => { + const band = __walk('[data-region="dashboard"], .rd-shell')[0]; + const b = band ? Math.round(band.getBoundingClientRect().bottom) : 0; + return Math.min(window.innerHeight - 30, Math.max(b + 20, 120)); + })()`) + const below = { x: 150, y: belowY } const zoomBefore2 = await page.evaluate(`(() => { const m = window.__rdashMap; return m ? m.getZoom() : null })()`) const scrollBefore2 = await gridTop() await page.mouse.move(below.x, below.y)