mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
test(desktop): media-corpus classifier — media reflow is absorbed-class under w4a-gate-1
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>
This commit is contained in:
co-authored by
Tyler Longwell
parent
2bd2076ebb
commit
b0005dfc49
@@ -2255,6 +2255,40 @@ const mockChannels: MockChannel[] = [
|
||||
createMockMember(MOCK_IDENTITY_PUBKEY, "member", 1900),
|
||||
],
|
||||
}),
|
||||
// Media-corpus channel for the W4a upscroll gate's MEDIA class
|
||||
// (upscroll-media.perf.ts). Seeded with 400 rows whose realization error
|
||||
// comes from MEDIA reserve-vs-true mismatch, not text mis-estimation:
|
||||
// synchronous link-preview cards reserved flat at PREVIEW_CARD=70 but
|
||||
// rendered taller, and dim-carrying images whose reserved scaled box differs
|
||||
// from the rendered `h-auto object-contain` height. Deliberately does NOT
|
||||
// lean on dim-less image decode — that path is pinned to a fixed 256px box
|
||||
// (markdown/utils.ts:resolveImageReserveBox) and provably cannot reflow.
|
||||
// Its own channel so the jitter-corpus seed stays undisturbed.
|
||||
createMockChannel({
|
||||
id: "feedf00d-0000-4000-8000-000000000009",
|
||||
name: "media-corpus",
|
||||
channel_type: "stream",
|
||||
visibility: "open",
|
||||
description: "Media-heavy history for the upscroll gate's media class",
|
||||
topic: null,
|
||||
purpose: null,
|
||||
last_message_at: isoMinutesAgo(1),
|
||||
archived_at: null,
|
||||
created_by: ALICE_PUBKEY,
|
||||
topic_set_by: null,
|
||||
topic_set_at: null,
|
||||
purpose_set_by: null,
|
||||
purpose_set_at: null,
|
||||
topic_required: false,
|
||||
max_members: null,
|
||||
nip29_group_id: null,
|
||||
created_minutes_ago: 2000,
|
||||
updated_minutes_ago: 1,
|
||||
members: [
|
||||
createMockMember(ALICE_PUBKEY, "owner", 2000),
|
||||
createMockMember(MOCK_IDENTITY_PUBKEY, "member", 1900),
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const mockMessages = new Map<string, RelayEvent[]>();
|
||||
@@ -2988,6 +3022,98 @@ function jitterCorpusBody(i: number): string {
|
||||
}
|
||||
}
|
||||
|
||||
// A 1x1 transparent PNG as a data URL. Decodes synchronously and reliably in
|
||||
// both headless engines, so a seeded media row realizes a real rendered box
|
||||
// regardless of network — the mock harness never serves image bytes. The
|
||||
// intrinsic 1x1 shape is irrelevant to layout: dim-carrying images get explicit
|
||||
// width/height attributes (markdown.tsx:1528/1533) that drive the reserved
|
||||
// aspect box, and `object-contain` letterboxes the 1x1 inside it.
|
||||
const TINY_PNG_DATA_URL =
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M8AAAMBAQDJ/1p3AAAAAElFTkSuQmCC";
|
||||
|
||||
// Body + imeta-tag generator for the media-corpus seed. Eight row kinds cycled
|
||||
// by index. The realization error is MEDIA reserve-vs-true mismatch, from three
|
||||
// sources verified in-source (thread: Eva's premise verification, mainbase
|
||||
// 2cc0eb53), NOT text mis-estimation:
|
||||
//
|
||||
// A. Link-preview cards (kinds 1-3): parsed synchronously from URL TEXT
|
||||
// (parseSupportedLinkPreview — no network). Card renders at mount at its
|
||||
// true height while estimateRowHeight reserved a flat PREVIEW_CARD=70. A
|
||||
// title + provider + type label renders taller than 70 → GROW. Deterministic.
|
||||
// B. Width-clamp images (kinds 4-5): CORRECT dims, but the estimator hardcodes
|
||||
// MEDIA_MAX_WIDTH=384 while the render clamps to `max-w-[min(24rem,100%)]`.
|
||||
// In a timeline column narrower than 384px the rendered image is width-
|
||||
// limited SHORTER than the estimator's 384-based scale reserved → SHRINK,
|
||||
// with no lying dim and no decode timing. (Eva's third divergence source.)
|
||||
// C. Dim-mismatch images, BOTH directions (kinds 6-7): a dim that OVERSTATES
|
||||
// height (reserve tall, render short → SHRINK) and one that UNDERSTATES
|
||||
// (reserve short, render tall → GROW). Seeded in both directions so the
|
||||
// corpus CAN produce SHRINK as well as GROW — a corpus that only makes GROW
|
||||
// can't falsify the GROW-dominant prediction.
|
||||
// Control (kind 0): short prose, no media — the calibration row.
|
||||
//
|
||||
// Deliberately NO dim-less-only image band: that path is pinned to a fixed
|
||||
// 256px box (resolveImageReserveBox → useFixedReserveBox), reserve==mount, and
|
||||
// provably cannot reflow. Including it would only add rows that never move.
|
||||
function mediaCorpusBody(i: number): { content: string; imeta?: string[] } {
|
||||
const url = (tag: string) => `${TINY_PNG_DATA_URL}#${tag}-${i}`;
|
||||
switch (i % 8) {
|
||||
case 0:
|
||||
// Control — no media, estimator close.
|
||||
return { content: `media note ${i}` };
|
||||
case 1:
|
||||
// GitHub PR link: supported preview card.
|
||||
return {
|
||||
content: `landed the fix — https://github.com/block/buzz/pull/${1600 + i}`,
|
||||
};
|
||||
case 2:
|
||||
// Linear issue link: supported preview card.
|
||||
return {
|
||||
content: `tracking this in https://linear.app/block/issue/BUZZ-${i}/upscroll-anchor-reversal-in-the-slow-momentum-regime`,
|
||||
};
|
||||
case 3:
|
||||
// Google Docs link: supported preview card.
|
||||
return {
|
||||
content: `notes are in https://docs.google.com/document/d/1AbCdEfGhIjKlMnOpQrStUvWxYz${i}/edit`,
|
||||
};
|
||||
case 4: {
|
||||
// Width-clamp SHRINK: a wide image with a CORRECT dim. Estimator scales
|
||||
// 900x600 against 384 → reserves ~256 (height-capped); a column < 384px
|
||||
// wide renders it shorter than that. Width clamp, no lying dim.
|
||||
const u = url("wide");
|
||||
return {
|
||||
content: `wide shot ${i}:\n`,
|
||||
imeta: [`url ${u}`, "m image/png", "dim 900x600"],
|
||||
};
|
||||
}
|
||||
case 5: {
|
||||
// Width-clamp SHRINK (portrait): 480x900 scales to ~256 tall at 384 wide,
|
||||
// but a narrower column scales width-first and renders shorter.
|
||||
const u = url("tall");
|
||||
return {
|
||||
content: `portrait ${i}:\n`,
|
||||
imeta: [`url ${u}`, "m image/png", "dim 480x900"],
|
||||
};
|
||||
}
|
||||
case 6: {
|
||||
// Dim OVERSTATES height → reserve tall, render short → SHRINK.
|
||||
const u = url("overstate");
|
||||
return {
|
||||
content: `overstated ${i}:\n`,
|
||||
imeta: [`url ${u}`, "m image/png", "dim 200x800"],
|
||||
};
|
||||
}
|
||||
default: {
|
||||
// Dim UNDERSTATES height → reserve short, render tall → GROW.
|
||||
const u = url("understate");
|
||||
return {
|
||||
content: `understated ${i}:\n`,
|
||||
imeta: [`url ${u}`, "m image/png", "dim 800x120"],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getMockMessageStore(channelId: string): RelayEvent[] {
|
||||
const existing = mockMessages.get(channelId);
|
||||
if (existing) {
|
||||
@@ -3189,7 +3315,32 @@ function getMockMessageStore(channelId: string): RelayEvent[] {
|
||||
content: jitterCorpusBody(index),
|
||||
sig: "mocksig".repeat(20).slice(0, 128),
|
||||
}))
|
||||
: [];
|
||||
: channelId === "feedf00d-0000-4000-8000-000000000009"
|
||||
? // Media corpus for the W4a gate's media class. Row height error
|
||||
// is MEDIA reserve-vs-true mismatch (link-preview cards flat-
|
||||
// reserved at PREVIEW_CARD=70 but rendered taller; dim-lying
|
||||
// images whose object-contain render diverges from the reserved
|
||||
// scaled box) — not text mis-estimation. See mediaCorpusBody.
|
||||
Array.from({ length: 400 }, (_, index) => {
|
||||
const body = mediaCorpusBody(index);
|
||||
return {
|
||||
id: `mock-media-${index}`,
|
||||
pubkey:
|
||||
index % 2 === 0 ? ALICE_PUBKEY : MOCK_IDENTITY_PUBKEY,
|
||||
created_at:
|
||||
Math.floor(Date.now() / 1000) - (400 - index) * 60,
|
||||
kind: 9,
|
||||
tags: body.imeta
|
||||
? [
|
||||
["h", channelId],
|
||||
["imeta", ...body.imeta],
|
||||
]
|
||||
: [["h", channelId]],
|
||||
content: body.content,
|
||||
sig: "mocksig".repeat(20).slice(0, 128),
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
mockMessages.set(channelId, seeded);
|
||||
return seeded;
|
||||
|
||||
@@ -0,0 +1,518 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { installMockBridge } from "../helpers/bridge";
|
||||
|
||||
/**
|
||||
* Media-corpus reversal characterization — classify MEDIA reflow snaps.
|
||||
*
|
||||
* WHY THIS EXISTS. The jitter corpus mis-estimates TEXT row heights; this one
|
||||
* mis-reserves MEDIA. The question this fixture answers (Eva's ruling, thread
|
||||
* event 0a496379): are the −240px media reflow snaps Tyler may feel on scroll-up
|
||||
* momentum-class (already killed by the w4a-gate-1 rekey → SKIP), walk-blind /
|
||||
* UNATTRIBUTED (a separate RO-path leg), or SHRINK-class (needs Max's pre-
|
||||
* realization band)? It reuses the fast classifier's machinery VERBATIM — the
|
||||
* `probeLen` append-count join, the SKIP/GROW/SHRINK/UNATTRIBUTED discriminator,
|
||||
* `dev`, and the ±2-frame `firedNear` admission flag — and only swaps the corpus,
|
||||
* narrows the viewport so the width-clamp SHRINK source is active, and relaxes
|
||||
* the SKIP-only admission assertion (media survivors are not all SKIP).
|
||||
*
|
||||
* THE CORPUS (channel `media-corpus`, e2eBridge `mediaCorpusBody`). Row height
|
||||
* error is MEDIA reserve-vs-true mismatch, not text mis-estimation, from three
|
||||
* in-source-verified divergence sources (Eva's premise verification, mainbase
|
||||
* 2cc0eb53):
|
||||
* A. Link-preview cards — reserved flat at PREVIEW_CARD=70, rendered taller → GROW.
|
||||
* B. 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 NARROWER than 384px the render is width-limited shorter → SHRINK.
|
||||
* This fixture MEASURES the container width and asserts it is < 384 so the
|
||||
* clamp provably bites (else the SHRINK source is silently inert).
|
||||
* C. Dim-mismatch images, BOTH directions — overstate height → SHRINK, understate
|
||||
* → GROW; seeded both ways so the corpus CAN produce SHRINK, making the
|
||||
* GROW-dominant prediction falsifiable.
|
||||
* No dim-less band: that path is pinned to a fixed 256px box (reserve==mount) and
|
||||
* provably cannot reflow.
|
||||
*
|
||||
* PREDICTIONS ON RECORD (classifier decides): Dawn GROW-likely, Eva SHRINK-likely.
|
||||
*
|
||||
* HONESTY BOUND: this is a deterministic proxy for media reflow — a committed,
|
||||
* re-runnable classification of reserve-vs-true mismatch — NOT a claim to
|
||||
* reproduce Tyler's exact frames. His live trackpad still owns acceptance. The
|
||||
* WebKit `dScroll=0.0` coalesced still frame is the real-device phenomenon; the
|
||||
* synthetic `mouse.wheel` drive surfaces the bounded survivors to rule on.
|
||||
*/
|
||||
|
||||
// Fast constant drive — identical to the W4a gate (`upscroll-raf-correction`),
|
||||
// so this fixture surfaces the same bounded survivors the gate leaves.
|
||||
const WHEEL_DELTA = 12; // px/event — matches the gate's constant velocity
|
||||
const WHEEL_PERIOD_MS = 32; // gate cadence (~375px/s)
|
||||
const DURATION_MS = 12_000;
|
||||
const SAFE_MARGIN = 100;
|
||||
// Same reversal definition as the gate: row moving against the scroll by more
|
||||
// than staircase noise. Upscroll → rowMove normally >= 0, so a genuine
|
||||
// against-direction move is < -REVERSAL_PX.
|
||||
const REVERSAL_PX = 3;
|
||||
// Must equal `ANCHOR_BUILD_STAMP` in `useAnchoredScroll.ts` — stale-`dist`
|
||||
// guard (see the gate fixture). Bump BOTH together per experiment.
|
||||
const EXPECTED_BUILD_STAMP = "w4a-gate-1";
|
||||
// Narrow window so the timeline column lands below MEDIA_MAX_WIDTH=384 and the
|
||||
// width-clamp SHRINK source (divergence B) is active. Measured in-test below.
|
||||
const NARROW_VIEWPORT = { width: 450, height: 720 };
|
||||
const MEDIA_MAX_WIDTH = 384; // must match rowHeightEstimate.ts
|
||||
|
||||
type Frame = {
|
||||
t: number;
|
||||
top: number | null;
|
||||
scrollTop: number;
|
||||
mounted: number;
|
||||
rowId: string | null;
|
||||
probeLen: number;
|
||||
};
|
||||
|
||||
test("W4a media-classify: MEDIA reflow class mix (SKIP/GROW/SHRINK)", async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
await installMockBridge(page);
|
||||
page.on("console", (m) => {
|
||||
if (m.type() === "error") console.log("PAGE ERROR:", m.text());
|
||||
});
|
||||
page.on("pageerror", (e) => console.log("PAGE EXCEPTION:", e.message));
|
||||
await page.addInitScript(() => {
|
||||
(
|
||||
globalThis as unknown as { __ANCHOR_PROBE__: unknown[] }
|
||||
).__ANCHOR_PROBE__ = [];
|
||||
});
|
||||
await page.goto("/");
|
||||
await page.waitForFunction(
|
||||
() => typeof window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__ === "function",
|
||||
);
|
||||
// Navigate at the default (wide) viewport so the sidebar renders and the
|
||||
// channel is clickable, THEN narrow — a narrow window collapses the sidebar.
|
||||
await page.getByTestId("channel-media-corpus").click();
|
||||
await page.setViewportSize(NARROW_VIEWPORT);
|
||||
const timeline = page.getByTestId("message-timeline");
|
||||
await page.waitForFunction(() => {
|
||||
const el = document.querySelector(
|
||||
'[data-testid="message-timeline"]',
|
||||
) as HTMLDivElement | null;
|
||||
return !!el && el.scrollHeight > el.clientHeight + 1000;
|
||||
});
|
||||
|
||||
// The width-clamp SHRINK source is only active if the rendered image
|
||||
// container is narrower than the estimator's hardcoded MEDIA_MAX_WIDTH=384.
|
||||
// Measure a mounted image block and assert it: if the column is wide the
|
||||
// clamp is inert and any SHRINK-absence below would prove nothing.
|
||||
const mediaBoxWidth = await page.evaluate(() => {
|
||||
const box = document.querySelector<HTMLElement>(
|
||||
'[data-testid="message-timeline"] [data-block-media]',
|
||||
);
|
||||
return box ? Math.round(box.getBoundingClientRect().width) : null;
|
||||
});
|
||||
expect(
|
||||
mediaBoxWidth,
|
||||
"no media block mounted in the timeline — corpus did not realize images",
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
mediaBoxWidth as number,
|
||||
`media container width ${mediaBoxWidth}px is not below MEDIA_MAX_WIDTH=${MEDIA_MAX_WIDTH} — width-clamp SHRINK source is inert; narrow the viewport further`,
|
||||
).toBeLessThan(MEDIA_MAX_WIDTH);
|
||||
|
||||
await timeline.evaluate((element) => {
|
||||
const el = element as HTMLDivElement;
|
||||
el.style.overflowAnchor = "none";
|
||||
el.scrollTop = el.scrollHeight;
|
||||
el.dispatchEvent(new Event("scroll", { bubbles: true }));
|
||||
});
|
||||
await page.waitForTimeout(200);
|
||||
await timeline.hover();
|
||||
|
||||
// Per-frame sampler (identical row-tracking to the gate, plus a join key into
|
||||
// the hook's correction probe). The fixture sampler and the hook's rAF sampler
|
||||
// are SEPARATE rAF loops, so "the correction for frame i" is not reliably the
|
||||
// same-tick probe entry (two rAF callbacks in one frame fire in registration
|
||||
// order, which we don't control). The order-robust join is by APPEND COUNT:
|
||||
// each frame records `probeLen` (the correction-probe array length at that
|
||||
// tick) and the signed shift + fire flag of any attempts that appended since
|
||||
// the previous frame. A reversal between frame i-1 and i is then attributed to
|
||||
// the attempts in that interval — no same-tick ordering assumption.
|
||||
await timeline.evaluate((element, margin: number) => {
|
||||
const el = element as HTMLDivElement;
|
||||
const w = window as unknown as {
|
||||
__PROBE__: { frames: Frame[]; stop: boolean };
|
||||
};
|
||||
const g = globalThis as unknown as {
|
||||
__ANCHOR_PROBE__?: Array<{
|
||||
wouldFire: boolean;
|
||||
residual: number;
|
||||
signedShift: number;
|
||||
}>;
|
||||
};
|
||||
type Frame = {
|
||||
t: number;
|
||||
top: number | null;
|
||||
scrollTop: number;
|
||||
mounted: number;
|
||||
rowId: string | null;
|
||||
// Correction-probe array length at this tick — the append-count join key.
|
||||
probeLen: number;
|
||||
};
|
||||
w.__PROBE__ = { frames: [], stop: false };
|
||||
let trackedId: string | null = null;
|
||||
const pick = (): string | null => {
|
||||
const box = el.getBoundingClientRect();
|
||||
for (const row of el.querySelectorAll<HTMLElement>("[data-message-id]")) {
|
||||
const rect = row.getBoundingClientRect();
|
||||
if (rect.top > box.top + margin && rect.bottom < box.bottom - margin) {
|
||||
return row.dataset.messageId ?? null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const tick = (t: number) => {
|
||||
if (w.__PROBE__.stop) return;
|
||||
const mounted = el.querySelectorAll("[data-message-id]").length;
|
||||
let top: number | null = null;
|
||||
if (trackedId) {
|
||||
const row = el.querySelector<HTMLElement>(
|
||||
`[data-message-id="${CSS.escape(trackedId)}"]`,
|
||||
);
|
||||
if (row) {
|
||||
const rect = row.getBoundingClientRect();
|
||||
const box = el.getBoundingClientRect();
|
||||
const inBand =
|
||||
rect.top > box.top + margin && rect.bottom < box.bottom - margin;
|
||||
top = inBand ? rect.top : null;
|
||||
}
|
||||
}
|
||||
if (top === null) trackedId = pick();
|
||||
w.__PROBE__.frames.push({
|
||||
t,
|
||||
top,
|
||||
scrollTop: el.scrollTop,
|
||||
mounted,
|
||||
rowId: trackedId,
|
||||
probeLen: g.__ANCHOR_PROBE__?.length ?? 0,
|
||||
});
|
||||
requestAnimationFrame(tick);
|
||||
};
|
||||
requestAnimationFrame(tick);
|
||||
}, SAFE_MARGIN);
|
||||
|
||||
// Fast constant drive — matches the W4a gate exactly, so the same bounded
|
||||
// survivors surface. No decay: this is the fast regime, not Tyler's slow one.
|
||||
const started = Date.now();
|
||||
while (Date.now() - started < DURATION_MS) {
|
||||
await page.mouse.wheel(0, -WHEEL_DELTA);
|
||||
await page.waitForTimeout(WHEEL_PERIOD_MS);
|
||||
}
|
||||
|
||||
const frames: Frame[] = await timeline.evaluate((_el) => {
|
||||
const w = window as unknown as {
|
||||
__PROBE__: { frames: Frame[]; stop: boolean };
|
||||
};
|
||||
type Frame = {
|
||||
t: number;
|
||||
top: number | null;
|
||||
scrollTop: number;
|
||||
mounted: number;
|
||||
rowId: string | null;
|
||||
probeLen: number;
|
||||
};
|
||||
w.__PROBE__.stop = true;
|
||||
return w.__PROBE__.frames;
|
||||
});
|
||||
|
||||
const { corrections, buildStamp } = await page.evaluate(() => {
|
||||
const g = globalThis as unknown as {
|
||||
__ANCHOR_PROBE__?: Array<{
|
||||
source: "raf" | "ro";
|
||||
wouldFire: boolean;
|
||||
residual: number;
|
||||
signedShift: number;
|
||||
renderedScroll?: number;
|
||||
}>;
|
||||
__ANCHOR_BUILD_STAMP__?: string;
|
||||
};
|
||||
return {
|
||||
corrections: g.__ANCHOR_PROBE__ ?? [],
|
||||
buildStamp: g.__ANCHOR_BUILD_STAMP__ ?? null,
|
||||
};
|
||||
});
|
||||
|
||||
// Media-reflow census. The felt question is not "did any correction fire"
|
||||
// (text rows fire too) but "did an above-anchor MEDIA reflow, on a row the eye
|
||||
// is tracking, render a backward snap under the gate fix?" So for every frame
|
||||
// interval carrying a real reflow attempt (|signedShift| > REFLOW_PX) we record
|
||||
// whether the tracked row was in-band across it and what it actually did
|
||||
// (rowMove). An in-band reflow with rowMove ≈ 0 or forward is an ABSORBED media
|
||||
// reflow — the corpus produced the divergence and the gate+correction ate it.
|
||||
// Consecutive frames carrying the SAME (row, sign, fired) reflow are collapsed
|
||||
// to one census entry: a large above-anchor gap the momentum gate holds is
|
||||
// re-measured every rAF, and counting each frame would inflate one persistent
|
||||
// reflow into hundreds. We keep the WORST (most-backward) rowMove of the run so
|
||||
// a hidden snap inside a persistent reflow still surfaces.
|
||||
const REFLOW_PX = 3;
|
||||
type ReflowFrame = {
|
||||
i: number;
|
||||
shift: number;
|
||||
fired: boolean;
|
||||
inBand: boolean;
|
||||
worstRowMove: number | null;
|
||||
frames: number;
|
||||
rowId: string | null;
|
||||
};
|
||||
const mediaReflows: ReflowFrame[] = [];
|
||||
for (let i = 1; i < frames.length; i += 1) {
|
||||
const a = frames[i - 1];
|
||||
const b = frames[i];
|
||||
const attempts = corrections.slice(a.probeLen, b.probeLen);
|
||||
let biggest: (typeof attempts)[number] | null = null;
|
||||
for (const c of attempts) {
|
||||
if (Math.abs(c.signedShift) <= REFLOW_PX) continue;
|
||||
if (
|
||||
biggest === null ||
|
||||
Math.abs(c.signedShift) > Math.abs(biggest.signedShift)
|
||||
) {
|
||||
biggest = c;
|
||||
}
|
||||
}
|
||||
if (biggest === null) continue;
|
||||
const inBand =
|
||||
a.top !== null &&
|
||||
b.top !== null &&
|
||||
a.rowId !== null &&
|
||||
a.rowId === b.rowId;
|
||||
const rowMove = inBand ? (b.top as number) - (a.top as number) : null;
|
||||
const sign = Math.sign(biggest.signedShift);
|
||||
const prev = mediaReflows[mediaReflows.length - 1];
|
||||
// Collapse a run: same tracked row, same reflow sign, same fire decision.
|
||||
if (
|
||||
prev &&
|
||||
prev.rowId === b.rowId &&
|
||||
Math.sign(prev.shift) === sign &&
|
||||
prev.fired === biggest.wouldFire &&
|
||||
prev.inBand === inBand
|
||||
) {
|
||||
prev.frames += 1;
|
||||
if (Math.abs(biggest.signedShift) > Math.abs(prev.shift)) {
|
||||
prev.shift = biggest.signedShift;
|
||||
}
|
||||
if (
|
||||
rowMove !== null &&
|
||||
(prev.worstRowMove === null || rowMove < prev.worstRowMove)
|
||||
) {
|
||||
prev.worstRowMove = rowMove;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
mediaReflows.push({
|
||||
i,
|
||||
shift: biggest.signedShift,
|
||||
fired: biggest.wouldFire,
|
||||
inBand,
|
||||
worstRowMove: rowMove,
|
||||
frames: 1,
|
||||
rowId: b.rowId,
|
||||
});
|
||||
}
|
||||
const inBandReflows = mediaReflows.filter((r) => r.inBand);
|
||||
|
||||
// Score same-row frame pairs. A reversal is rowMove <= -REVERSAL_PX. For each
|
||||
// reversal, join to the correction attempt(s) that appended to the hook probe
|
||||
// BETWEEN frame i-1 and i (append-count window: probe indices [a.probeLen,
|
||||
// b.probeLen)). Classify by the SIGN of aboveShift + whether the write fired —
|
||||
// the three-way discriminator Sami specced (thread event 5b46582e):
|
||||
// • wouldFire == false → SKIP: momentum gate (:29) / cross-
|
||||
// check (:451) suppressed the write. The reversal is UNCORRECTED reflow;
|
||||
// absorption never got to act. A 27→27 here = wiring/gate, not physics.
|
||||
// • fired, signedShift > 0 (GROW) → content above grew, anchor shoved
|
||||
// DOWN, the correction WRITE is the felt backward snap. Absorption's
|
||||
// amortizable topology — deferring the pullback into forward frames helps.
|
||||
// • fired, signedShift < 0 (SHRINK) → content above shrank, the reflow
|
||||
// ITSELF pulls the anchor up and renders the reversal before any write.
|
||||
// Structurally uncorrectable by us; only smaller per-frame realization
|
||||
// (Max's pre-realization band / contain-intrinsic-size) shrinks it.
|
||||
// A reversal with no attempt in its window is UNATTRIBUTED (the correcting
|
||||
// observer's attempt landed in an adjacent frame under rAF interleave) — we
|
||||
// count it separately rather than force it into a class.
|
||||
let scored = 0;
|
||||
let reanchors = 0;
|
||||
type Klass = "skip" | "grow" | "shrink" | "unattributed";
|
||||
const reversals: Array<{
|
||||
i: number;
|
||||
rowMove: number;
|
||||
dScroll: number;
|
||||
dev: number; // Leg 5: rowMove − scrollDelta (rendered deviation from tracking)
|
||||
signedShift: number | null;
|
||||
fired: boolean;
|
||||
klass: Klass;
|
||||
// Widen-independent admission flag: any wouldFire=true record in a WIDER
|
||||
// ±2-frame append-count window than the ±1 attribution window. If false,
|
||||
// no fired write sits near this reversal at any reasonable width → SKIP is
|
||||
// robust to the widen. If true, the class attribution's largest-|shift| rule
|
||||
// could have labelled it grow/shrink and the SKIP bin is in question.
|
||||
firedNear: boolean;
|
||||
rowId: string | null;
|
||||
}> = [];
|
||||
for (let i = 1; i < frames.length; i += 1) {
|
||||
const a = frames[i - 1];
|
||||
const b = frames[i];
|
||||
if (
|
||||
a.top === null ||
|
||||
b.top === null ||
|
||||
a.rowId === null ||
|
||||
a.rowId !== b.rowId
|
||||
) {
|
||||
reanchors += 1;
|
||||
continue;
|
||||
}
|
||||
scored += 1;
|
||||
const rowMove = b.top - a.top;
|
||||
const dScroll = b.scrollTop - a.scrollTop;
|
||||
if (rowMove > -REVERSAL_PX) continue;
|
||||
// Attribution window. The hook's correction attempt for the reflow that
|
||||
// produced this reversal can append across a ±1-frame span relative to our
|
||||
// sampler: the two rAF loops interleave in an order we don't control, and on
|
||||
// WebKit a late RO appends a frame after the reflow paints. So the window is
|
||||
// [prev-frame probeLen, NEXT-frame probeLen) — attempts from the frame
|
||||
// before through the frame after. A reversal with NO attempt anywhere in
|
||||
// that span is genuinely unattributed (the corrector did not run a mid-
|
||||
// history attempt on those frames at all — e.g. re-pick guard or null cur),
|
||||
// which is itself a distinct diagnosis from a fired-then-clamped write.
|
||||
const next = frames[i + 1] ?? b;
|
||||
const window = corrections.slice(a.probeLen, next.probeLen);
|
||||
let attempt: (typeof corrections)[number] | null = null;
|
||||
for (const c of window) {
|
||||
if (
|
||||
attempt === null ||
|
||||
Math.abs(c.signedShift) > Math.abs(attempt.signedShift)
|
||||
) {
|
||||
attempt = c;
|
||||
}
|
||||
}
|
||||
let klass: Klass;
|
||||
if (attempt === null) {
|
||||
klass = "unattributed";
|
||||
} else if (!attempt.wouldFire) {
|
||||
klass = "skip";
|
||||
} else {
|
||||
klass = attempt.signedShift >= 0 ? "grow" : "shrink";
|
||||
}
|
||||
// Leg 5 rendered deviation from pure scroll-tracking. A correctly-anchored
|
||||
// row moves only with scroll (rowMove == scrollDelta), so any deviation is
|
||||
// the corrector's footprint — or, on a SKIP, its ABSENCE.
|
||||
const dev = rowMove - dScroll;
|
||||
// Widen-independent admission check. Look one frame WIDER than the ±1
|
||||
// attribution window ([i-2 .. i+2] via probeLen) and ask only: is there ANY
|
||||
// fired write in that neighborhood? This does not pick a single attempt or
|
||||
// depend on the largest-|shift| tie-break, so it cannot be flipped by the
|
||||
// widen. A SKIP survivor must have firedNear=false: no write could be the
|
||||
// backward mover if none fired near the frame at all.
|
||||
const lo = frames[i - 2] ?? a;
|
||||
const hi = frames[i + 2] ?? next;
|
||||
const neighborhood = corrections.slice(lo.probeLen, hi.probeLen);
|
||||
const firedNear = neighborhood.some((c) => c.wouldFire);
|
||||
reversals.push({
|
||||
i,
|
||||
rowMove,
|
||||
dScroll,
|
||||
dev,
|
||||
signedShift: attempt?.signedShift ?? null,
|
||||
fired: attempt?.wouldFire ?? false,
|
||||
klass,
|
||||
firedNear,
|
||||
rowId: b.rowId,
|
||||
});
|
||||
}
|
||||
|
||||
const maxReversalPx =
|
||||
reversals.length === 0
|
||||
? 0
|
||||
: Math.max(...reversals.map((r) => Math.abs(r.rowMove)));
|
||||
// A reversal on a near-still frame (|dScroll| < REVERSAL_PX) is the felt case:
|
||||
// the eye is barely moving, so a backward row snap is maximally visible.
|
||||
const stillFrameReversals = reversals.filter(
|
||||
(r) => Math.abs(r.dScroll) < REVERSAL_PX,
|
||||
);
|
||||
const byClass = (k: Klass) => reversals.filter((r) => r.klass === k).length;
|
||||
|
||||
/* eslint-disable no-console */
|
||||
console.log("\n=== W4a MEDIA-CORPUS REFLOW CLASS MIX ===");
|
||||
console.log(`engine: ${browserName}`);
|
||||
console.log(`build stamp: ${buildStamp ?? "(absent)"}`);
|
||||
console.log(`frames sampled: ${frames.length}`);
|
||||
console.log(`frame-pairs scored: ${scored}`);
|
||||
console.log(`re-anchor/skip frames: ${reanchors}`);
|
||||
console.log(`reversal frames: ${reversals.length}`);
|
||||
console.log(` of which still-frame: ${stillFrameReversals.length}`);
|
||||
console.log(`max reversal px: ${maxReversalPx.toFixed(1)}`);
|
||||
console.log("--- reversal class mix (Sami's discriminator) ---");
|
||||
console.log(` SKIP (gate/xcheck, uncorrected reflow): ${byClass("skip")}`);
|
||||
console.log(
|
||||
` GROW (fired, write is the snap — absorb): ${byClass("grow")}`,
|
||||
);
|
||||
console.log(
|
||||
` SHRINK (fired, reflow renders it — Max): ${byClass("shrink")}`,
|
||||
);
|
||||
console.log(
|
||||
` UNATTRIBUTED (no attempt in window): ${byClass("unattributed")}`,
|
||||
);
|
||||
for (const r of reversals
|
||||
.slice()
|
||||
.sort((x, y) => x.rowMove - y.rowMove)
|
||||
.slice(0, 12)) {
|
||||
const s = r.signedShift === null ? "n/a" : r.signedShift.toFixed(1);
|
||||
console.log(
|
||||
` frame ${r.i} rowMove=${r.rowMove.toFixed(1)} dScroll=${r.dScroll.toFixed(1)} dev=${r.dev.toFixed(1)} signedShift=${s} fired=${r.fired} firedNear=${r.firedNear} class=${r.klass} row=${r.rowId}`,
|
||||
);
|
||||
}
|
||||
console.log("========================================\n");
|
||||
|
||||
console.log("=== MEDIA-REFLOW CENSUS (|shift|>3px above-anchor reflows) ===");
|
||||
console.log(`distinct reflow runs: ${mediaReflows.length}`);
|
||||
console.log(` in-band (tracked row): ${inBandReflows.length}`);
|
||||
console.log(
|
||||
` GROW / SHRINK runs: ${mediaReflows.filter((r) => r.shift > 0).length} / ${mediaReflows.filter((r) => r.shift < 0).length}`,
|
||||
);
|
||||
for (const r of mediaReflows) {
|
||||
console.log(
|
||||
` frame ${r.i} shift=${r.shift.toFixed(1)} fired=${r.fired} inBand=${r.inBand} worstRowMove=${r.worstRowMove === null ? "n/a" : r.worstRowMove.toFixed(1)} frames=${r.frames} row=${r.rowId}`,
|
||||
);
|
||||
}
|
||||
console.log(
|
||||
"==============================================================\n",
|
||||
);
|
||||
/* eslint-enable no-console */
|
||||
|
||||
// Sanity: the actuation actually produced a scored upscroll.
|
||||
expect(scored).toBeGreaterThan(50);
|
||||
// Stale-`dist` guard — a characterization on a stale bundle misleads exactly
|
||||
// like a stale gate run. Assert the experiment's stamp ran.
|
||||
expect(buildStamp).toBe(EXPECTED_BUILD_STAMP);
|
||||
// MEDIA LIVENESS. Not "did any correction fire" (text rows fire too) but "did
|
||||
// the MEDIA corpus actually produce above-anchor reflows on tracked rows?" If
|
||||
// it did not, the zero-reversal result below is vacuous (a dead corpus proves
|
||||
// nothing). This asserts the corpus is a LIVE reflow source — the divergence
|
||||
// bands realize — so the absence of felt reversals is a real absorption
|
||||
// result, not a silent no-op.
|
||||
expect(
|
||||
inBandReflows.length,
|
||||
"no |shift|>3px media reflow landed on a tracked in-band row — corpus did not realize media divergence, the reversal census is vacuous",
|
||||
).toBeGreaterThan(0);
|
||||
|
||||
// --- SKIP ADMISSION GATE (Eva, thread event 2a4e31fa) -----------------------
|
||||
// Every reversal typed SKIP must have NO fired write in its ±2-frame
|
||||
// neighborhood. This is attribution-free: it does not depend on the ±1 widen
|
||||
// or the largest-|shift| tie-break, so a SKIP that survives it is robust to
|
||||
// the soft joint in the classifier. If any SKIP shows firedNear=true, a write
|
||||
// did land near the frame and the class attribution mis-labelled it — the bin
|
||||
// is not admissible and this fails loudly rather than passing a stale claim.
|
||||
// (Characterization otherwise; the reversal count itself is not gated.)
|
||||
const skips = reversals.filter((r) => r.klass === "skip");
|
||||
for (const r of skips) {
|
||||
expect(
|
||||
r.firedNear,
|
||||
`SKIP survivor frame ${r.i} (row ${r.rowId}) has a fired write in its ±2-frame neighborhood — attribution is not widen-robust, bin in question`,
|
||||
).toBe(false);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user