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:
npub1cc3ha7z055mu0rwwu7806t2wt8mj3pvu0uv5mfp2c50dahaqhczshdalg6
2026-07-09 02:09:16 -04:00
co-authored by Tyler Longwell
parent 2bd2076ebb
commit b0005dfc49
2 changed files with 670 additions and 1 deletions
+152 -1
View File
@@ -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![grab](${u})`,
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![grab](${u})`,
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![grab](${u})`,
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![grab](${u})`,
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;
+518
View File
@@ -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);
}
});