-
Notifications
You must be signed in to change notification settings - Fork 0
Review 5559
facebook/astryx#5559 · freddymeta · 4 files, +64/−2 LOOP VERSION: 1.5.0 · AUDIT RUBRIC: 1.12 · full lane · 2 critic passes Verdict: approve and merge — drafted only, nothing posted, nothing merged.
#5559 fix(Text, Heading): a truncated label shows one tooltip, not two — by freddymeta (bucket: collaborator — gh api .../collaborators/freddymeta/permission → write, and not in .github/ENGOWNERS or .github/DESIGNOWNERS. Treated as internal: he merges and follows up, so no Discord clause, and no [Full review] link, which is for owners-file teammates only.)
ed494011716ed9d8013de5acbfe2b4336ebff563 <- every claim below was verified at this commit; re-fetched at hand-off, unmoved
LOOP VERSION: 1.5.0 AUDIT RUBRIC: 1.12
Clean. Head repository owner is facebook, not a fork, so this is trusted code — and the diff is clean on its own terms anyway: 4 files, all source / test / changeset. No package.json script, no dependency or version bump, no pnpm-lock.yaml entry, nothing under .github/workflows/, no new *.sh/*.mjs/*.cjs under scripts/tools/.husky, nothing reading env, ~/.npmrc or ~/.ssh. Installed and built.
One thing worth naming and dismissing: the head commit is chore: untrack the node_modules symlink committed by mistake. A worktree-local node_modules symlink was committed mid-branch and removed in the same branch; git diff --stat origin/main...HEAD is the intended 4 files, +64/−2, so nothing rode along.
None. gh api repos/facebook/astryx/pulls/5559/reviews → empty; .../pulls/5559/comments → empty. The only issue comments are the Vercel and PR-Analysis bots. There is no colleague's conclusion to extend, narrow or contradict.
None. Of 205 open PRs, nothing else touches Text/Heading truncation or the double-tooltip shape. One PR is adjacent and does not conflict: #5398 fix(HoverCard,Tooltip): wait for the trigger to have a layout box before showing, which touches Tooltip/useTooltip.tsx and Layer/showWhenAnchored.ts — no file overlap with this diff, and it changes when a Tooltip may show, so the mount-window number in DISPOSITION 1 is measured against today's main and will move when #5398 lands.
LANE: full
WHY: fast fails on the visual condition. The visible endpoint this PR removes is the browser's own title tooltip — browser chrome, not page content — so no story renders it and no page screenshot in either arm can contain it, which means a green pr-visual cannot be coverage evidence for the endpoint that changed. The PR also carries needs:code-review. Every other fast condition did hold: one runtime decision, 2 runtime lines, no public/theme surface, no state or default, no Effect/listener/observer, no dependency, no architecture owner, no prior review, no conflict, no subsystem, no trust concern.
WHY 1: Hovering a clipped label draws two tooltips with the same words in them — Astryx's styled one, then the browser's plain one over the top of it a moment later. WHY 2: The person hovered because they wanted the words that got cut off. They get them twice, in two visual languages, the second arriving late and landing on the first. The reveal is the part that looks broken, so they are reading a glitch instead of a label. WHY 3: Truncation is the bargain a dense UI makes — stay dense, give the rest back on demand. When the give-back is what looks wrong, density has been bought with the component's credibility, which is the one thing that affordance existed to protect.
USER-FACING PROBLEM: Someone reading a clipped cell in a dense table hovers to see the rest and gets two overlapping tooltips saying exactly the same thing, the browser's plain one sliding over Astryx's after a beat. PROBLEM SEVERITY: harmful friction — the words are readable either way, so the task completes; what the person pays is a reveal that reads as a rendering fault. Not a broken task: nobody is blocked and nothing is unreachable.
VERDICT: clear
A clipped label needs some way to give back the words it cut off. Two separate things were doing that job at once, switched on by the same condition, so both appeared — the design system's own tooltip and the browser's built-in one. The change switches the browser's one off and keeps the design system's, which was already the thing telling assistive technology what the label says. So the label gives itself back once instead of twice.
SOLUTION (1 decision · ~2 runtime lines of 64)
- drop the native
title, keep theTooltip[the fix; traces to the stated problem]
The other 62 lines are a changeset and one test — evidence for the decision, not further decisions. R1j audit: the PR body carries a dedicated "Why the title goes and not the Tooltip" section, so the one decision has its stated reason. No unrecorded change.
BURDEN: low — two deleted JSX attributes. No new runtime state, no Effects, no listeners, timers or observers, no shared-system or global CSS coupling, no public surface, and one colocated test. BURDEN MATCH: proportionate — a deletion is the cheapest mechanism available for a duplicate-affordance problem, comfortably inside a friction-class budget.
RISK CLASS (R19): low-risk, all four: no new API surface (no prop, export or theme target) · no behavior regression I could measure — the change is the intended behavior change and every other path was driven identical · no performance or resource regression, zero Effects and nothing added on any of R18's four axes · nothing got bigger, no outer dimension moves.
COMMIT TYPE (R1f): honest. fix(...) claims consumer-visible change and the diff delivers exactly that, with a patch changeset. The repo's needs:code-review label ("new package/component/API") is the one claim the diff contradicts — there is no new package, component or API here — but it is a label, not the author's assertion, and it costs nothing but a human's signature.
VERDICT: clear
OWNER: Tooltip owns "show the full text"; Text/Heading own noticing the text is clipped (useTruncation). The diff moves the affordance wholly to the owner and leaves detection where it was.
TIER 1: the layer system — Tooltip in anchorRef (sibling) mode, Tooltip.tsx:255 (el.setAttribute(), which writes aria-describedby onto the anchor and merges with any existing id.
TIER 2: none.
SEAMS: a consumer's own Tooltip wrapping a truncated Text; an interactive descendant inside one; Table's default cell renderer, which wraps string cells in <Text maxLines={1}> at BaseTable.tsx:205 (<Text type="body" maxLines={1}>).
BEHAVIOR UNIT: inline — correct here. The diff deletes an attribute; there is no state machine, no Effect added, changed or removed, and nothing to extract.
| seam | driven result |
|---|---|
bare Text, hover |
works — 1 styled tooltip, full text, aria-describedby resolving to the full string |
bare Heading, hover |
works — same, 1 |
consumer's own Tooltip wrapping a truncated Text
|
consumer's tooltip wins, 1 visible, its own text — identical in both arms. After, the full text is not offered on this seam at all |
interactive descendant (<a> inside a truncated Text) |
works — hovering the link opens the parent's tooltip. Tab focuses the link and opens nothing, in both arms |
Table default cell renderer, textOverflow="truncate"
|
works — clipped cells go 3 titles → 0, hover still opens exactly 1 styled tooltip |
Table header cell |
not reached — headers set their own title on the <th> and are not wrapped in Text, BaseTable.tsx:463 (? {title: resolvedContent}) |
Does the implementation live with its owner and survive every documented seam? Yes. The one seam that changes character rather than surviving is the consumer-Tooltip case, and it improves there too: before, that composition showed the consumer's sentence and the browser's copy of the label — two different sentences at once.
Is this THE fix, or A fix? The fix. Root cause is two affordances for one job on one element, both gated on the same tooltipEnabled. An unevadable fix has exactly one, at the owner, which is what this does. The sharp test is the next place the same shape can occur — and core has exactly 3 non-test title={ sites outside this diff:
git grep -n "title={" -- packages/core/src '*.tsx' | grep -v test | grep -v stories → 3
None of the three pairs a title with a Tooltip on the same element. Chat/ChatToolCalls.tsx:417 and Chat/ChatMessageMetadata.tsx:155 sit in files that import no Tooltip at all (grep -c Tooltip → 0 for both); BaseTable.tsx:463 is a bare <th> title with no Text wrapper. The next instance is a different shape, which is what a genuinely scoped fix leaves behind.
VERDICT: clear
Everyone on the next release who hovers a clipped Text or Heading — which is every Table running textOverflow="truncate", since its default cell renderer wraps string cells in <Text maxLines={1}>. What they notice: the label opens once, instead of a second plainer copy sliding over the first a beat later. Builders notice nothing — no call site changes, and a title they pass themselves still wins.
What landing this newly exposes. In one truncate-mode Table you now get Astryx's styled tooltip on a body cell and the browser's own plain one on a column header. Measured on core-table--overflow-behavior: 4 of 8 <th> carry title in both arms, and hovering one opens 0 styled tooltips, while a clipped body cell opens 1. Before this PR both surfaces were native, so the mix read as consistent; after it, the same table answers a hover two different ways depending which row you are on. That is new — not still-broken. It is not the author's: BaseTable.tsx:463 predates the diff and sets title on every string header, truncated or not. No follow-up exists yet; the author's own note says Badge gets the same treatment next, so the class is already on their radar.
VERDICT: note — a newly visible cell/header asymmetry inside Table, pre-existing cause, named so approving is a choice about a known gap rather than a discovery later
No API change.
No prop, export, type, default or accepted value moves. title was never documented surface on either component — Text.doc.mjs and Heading.doc.mjs name it nowhere (grep -n title → 5 hits, every one the English word "titles" in prose). A consumer passing their own title is unaffected: it arrived through {...props} (Text.tsx:316) and still does.
OSSIFICATION: nothing is added, so nothing new is frozen. The interesting direction is the reverse — the diff un-ships a rendered attribute, and @astryxdesign/core is genuinely published (private unset, version 0.5.0), so there are real installed consumers who could in principle be selecting [title] or snapshotting it on a truncated Text. Repo-wide there is exactly one such assertion and it is Table's header (Table.test.tsx:1203-1205), which this does not touch; CI's test job is green on the exact head.
VERDICT: clear
No theming change, and no new theme targets.
STRUCTURAL ONLY on the two runtime files — no styles, no strings, no roles:
git diff origin/main...HEAD -- Text.tsx Heading.tsx | grep -E "^[+-]" \
| grep -cE "#[0-9a-fA-F]{3,8}|rgba?\(|hsla?\(|boxShadow|light-dark\(|stylex\.|xstyle" → 0
... | grep -cE "aria-|role=|useTranslator|t\('@astryx" → 0
VERDICT: clear
BEHAVIOR: yes, and it is the point. In the truncated state a rendered DOM attribute disappears. Measured two-sided, one worktree, one session: Text and Heading title full-string → null; Table truncate cells 3 of 6 → 0 of 6. There is a real consumer who could be broken — core is published at 0.5.0 and not private — so this is a genuine break and not an unreleased one. Walking the states the diff can reach: it adds no state, no default, no prop and no conditional; it deletes one JSX attribute. So empty / loading / error / disabled / controlled-vs-uncontrolled / boundary are not reachable by it. The one bound involved (tooltipEnabled) is untouched, so the far side is unchanged too: hasTruncateTooltip={false} and maxLines={0} had neither affordance before and have neither now.
API: no — nothing stops compiling, nothing changes meaning for an existing call site.
VISUAL: no. The two hovered frames are byte-identical (sha256 97fbb152…, both). Nothing grew: no width, height, padding, gap or min/max moves, and no element enters the flow, so there is no neighbour to reflow and no constrained-container case to run. What vanishes is browser chrome, painted outside the page's compositing surface.
THEME: no — no target, token or override is touched, and every existing target still points at the same element.
VERDICT: note — a deliberate, measured behavior break; it is the fix, and it is dispositioned in JUDGEMENT
EFFECTS: zero. None added, none changed, none removed, none moved. The diff is two deleted JSX attributes, so there is no Effect row to fill.
RENDER: one fewer attribute written per render while truncated. I did not benchmark it — there is no added work on any axis to price, and I would rather say that than call it negligible.
LISTENERS/OBSERVERS: unchanged. useTruncation still registers on the shared ResizeObserver singleton (observeResize); the diff does not touch that file.
LAYOUT: unchanged. No new forced read — the scrollWidth/offsetWidth and Range.getBoundingClientRect() reads all live in useTruncation, untouched, and none of them moved into render.
BUNDLE: −2 lines, no dependency.
VERDICT: clear — zero Effects, and nothing added on any of the four axes
VISUAL CHECK: manual frames required
WHY: pr-visual is green on the exact head — 19 of 19 check-runs green on ed49401, pr-a11y and pr-rtl among them — but it cannot be coverage evidence here. The visible endpoint this PR removes is the browser's own tooltip: chrome, not page content, so no Storybook story renders it and no page.screenshot() in either arm can contain it. An endpoint the gate structurally cannot cover promotes the decision to manual frames. Frames were taken for the endpoint pixels can carry — that the styled tooltip still opens, and is unchanged.
Before (title restored) |
After (PR head) |
|---|---|
![]() |
![]() |
SENSOR RECEIPT — build ed494011716ed9d8013de5acbfe2b4336ebff563 (observed == expected, both arms, one isolated exact-head worktree) · story zzreview-5559--text-plain · theme neutral · colorMode light · direction ltr · viewport 900×400 @1 · media: forcedColors false, reducedMotion false, coarsePointer false, hover true · targetCount 1, written from the story source (one <Text maxLines={1}> in a 300px box) before the page was opened · state {clipped: true, openTooltips: 1, tooltipText: "This is a very long label…", describedBy: "This is a very long label…"}, read through innerText and :popover-open, never textContent · runningAnimations 0 · zero page/console errors · state=pass on both arms, no expectation loosened. Receipts banked beside each PNG as *.sensors.json. Exact head: https://github.com/facebook/astryx/tree/ed494011716ed9d8013de5acbfe2b4336ebff563
ARM DIFF (before.diff, inspected): exactly two added lines, one per component, and the blob hashes are the exact inverse of the PR's own hunks — Heading d5039815847→5d6858a4c6f, Text 9c37c897fd2→9a130df9ac0. No unrelated declaration rode along.
Every difference, labelled. There is one candidate and it is no difference: the two PNGs are byte-identical, sha256 97fbb152b6722627da3fc77ad161d969a589be76e0bccf4a9b399f2307dee044 for both. Identical output is the expected contract here, not a sensor failure: the PR's own claim is "Nothing is lost but the duplicate", and the axis it moves is carried by a separate sensor that does differ across the arms — title = the full 84-character label before, null after. Both frames were opened with the read tool: a 300px bordered box with a clipped one-line label and the dark styled tooltip below it holding the full two-line string. Intentional, sourced to the PR body.
What pixels could not settle, said rather than implied: screencapture on this host returns could not create image from display, so the native tooltip itself was never photographed in either arm. Its presence is carried by the attribute sensor instead, and no sentence in the public review makes a visual claim about it.
VERDICT: clear
A1 accessible name — checked by me, driven. Unchanged. All 84 characters stay in the DOM in both arms (domTextLength: 84); CSS clips them visually. title was never what made the label readable to AT.
Description — checked by me, driven, both arms. The PR's central claim holds. aria-describedby lands on the anchor once the lazy Tooltip mounts and resolves to the full string, before and after, in all four driven stories; the merge path is Tooltip.tsx:255. In the consumer-Tooltip seam the id present is the consumer's, in both arms.
A5 focus — checked by me, driven, both arms. Neither arm opens anything on focus, and neither ever did: Text renders a <span>, which is not focusable, and with an <a> descendant Tab lands on the link with 0 tooltips open on both sides. Focus is never dropped to <body> and nothing focusable is removed to express state. Unchanged and pre-existing — not this PR's (R13).
A8/A9 hover-only — checked by me. Nothing becomes reachable only by hover: the text itself is in the DOM. Hover-only before, hover-only now.
A18 axe — auto-covered. pr-a11y green on the exact head, and the diff adds no entry to .github/a11y-baseline.json — it touches 4 files and that is not one of them.
I1/I4/I5/I6 strings — checked by me. No string added or removed. title carried truncation.fullText, i.e. the consumer's own children, never a catalog key; the i18n grep on the diff is 0 (THEMING slot).
I8–I12 direction — auto-covered. pr-rtl green on the exact head; the diff writes no CSS and no physical property.
VERDICT: clear
| slot | verdict |
|---|---|
| PROBLEM | clear |
| SOLUTION | clear |
| ARCHITECTURE | clear |
| IMPACT | note — newly visible cell/header asymmetry in Table
|
| API | clear |
| THEMING | clear |
| BREAKING | note — deliberate, measured behavior break |
| PERFORMANCE | clear |
| VISUAL | clear |
| A11Y & I18N | clear |
Reading the bodies and not the labels: no slot's prose contains a sentence a user would call a bug. The strongest candidate is DISPOSITION 1 below, and it describes a state both arms share once the pointer moves. Nothing is filed in the wrong slot — the two live facts belong to Table (IMPACT) and to Tooltip (DISPOSITION 1), and both are named against the line that causes them, not the line that describes them. The two notes do not compound: one is a pre-existing sibling gap, the other a pre-existing mount race; neither makes the other worse.
GOAL: met — measured two-sided in one worktree over HMR, arms distinguished only by before.diff. Hovering a clipped Text: before, title present + 1 styled tooltip = 2 shown; after, no title + 1 styled tooltip = 1. Same for Heading, for a Text with a link child, and inside a consumer's Tooltip. Inside Table at textOverflow="truncate", titles on clipped cells go 3 → 0 with the styled tooltip still opening. The PR's own table is reproduced, not taken on trust — and its test-plan claim is too: restoring the deleted line fails the new test on exactly that assertion — Text.test.tsx:338 ().not.toHaveAttribute('title');), 1 failed | 31 passed.
DISPOSITION
0. The behavior break itself — a rendered title disappears in the truncated state → blocks nothing; it is the fix. BREAKING is not "no" on the behavior axis, so it is dispositioned here rather than left in a slot: GOAL below is the evidence that the break is the intended one, and the only consumer-visible loss is the duplicate the PR set out to remove.
-
The lazy
Tooltipmissesmouseenterwhen it wires under a pointer already resting there → accepted; not this PR's, and the "before" half is unproven. Measured: 161–189 ms between the text being clipped on screen andaria-describedbyappearing. With the cursor parked and never moved, the styled tooltip stays shut for the full 5 s I watched in both arms, and one pixel of movement opens it in both. The only delta is thattitlewas present in that window and now is not — and whether Chrome paints a native tooltip for a pointer that never moved is precisely what this host cannot capture. Naming it, not charging it. #5398 is active work on when a Tooltip may show, so the number will move. -
Tableheaders keep their own nativetitle(BaseTable.tsx:463), so a truncate-mode table now mixes tooltip kinds → follow-up, not blocking. Pre-existing cause, independently landable, and stated in IMPACT so approving is a choice about a known gap. The intermediate state is acceptable: both surfaces still answer a hover with the right words, they just answer in different styling — nobody loses information while the follow-up is outstanding. Carried into the public comment in one clause with its anchor; no follow-up PR exists yet and this read-only run may not file one, so it is handed to Cindy rather than promised to the author. -
Headingtakes the identical deletion with no test of its own → accepted (nit).Heading.test.tsx:143(it('accepts maxLines prop', () => {) already coversmaxLines, so the file exists and the case would be two lines. Low risk: the class has two members and both were driven in real Chromium here,Headingincluded. An optional inline, not a hold.
ADVICE: omitted — nothing is being asked for. The two live facts are a follow-up and a nit; neither needs a remedy prescribed, and inventing one here would be authorship rather than review.
AUTHOR CAN PROCEED: yes — nothing blocks, no human-owned decision is open, no acceptance criteria are outstanding, and the fix is already complete at this head.
WORST OUTCOME: "the only delta is that title was present in that window and now is not" → note, compatible with approve. The sentence names a ~170 ms window, recovered by any pointer movement, in a state where the styled tooltip is equally shut before and after. Nothing anywhere in the slots says a person is stuck, cannot reach something, hears nothing, loses their place, or loses data.
JUDGEMENT NEEDED: none — a defect fixed against the existing contract. No API, no new concept, no theme target, no design choice, no measured cost to accept.
approve and merge
- [not blocking]
Tableheaders still set a nativetitlethemselves → hover a column header in a truncate-mode table and you get the browser's plain tooltip, while the cell one row below gives you Astryx's styled one ·packages/core/src/Table/BaseTable.tsx:463 - [not blocking]
Headingtakes the same deletion with no test of its own → the next person to touchHeading's truncation branch has nothing that fails if thetitlecomes back ·packages/core/src/Heading/Heading.test.tsx:143(it('accepts maxLines prop', () => {)
Thanks, good catch — dropping title rather than the Tooltip is the right way round. Merging as is. Table headers set their own title (BaseTable.tsx:463); ours to clean up.
[Reviewed by Robohands]
- packages/core/src/Heading/Heading.test.tsx:143 — Heading takes the same deletion. Might be worth the same test here.
- A truncated
Textwrapped in a consumer's ownTooltipshows only the consumer's string; the truncation tooltip never mounts. Identical in both arms, so pre-existing — but after this, the full text is not offered on that seam at all. - Keyboard users have never reached a truncated label's full text: the anchor is a non-focusable
<span>, and focusing an<a>inside it opens nothing in either arm. Unchanged, not the author's. -
Tableheaders settitleon every string header, truncated or not — so a header whose text fits still gets a browser tooltip repeating what is already on screen. Adjacent to finding 1, worse than it, and entirely pre-existing.
All against the isolated exact-head worktree at ed49401, storybook dev on port 6259, both arms in the same worktree over HMR. The before arm is before.diff applied; the after arm is the head untouched.
2 vs 1 tooltips; describedby; focus probe-kit/text-truncation-tooltip-5559.cjs
ARM=before|after SB_PORT=6259
161–189 ms mount window; parked cursor probe-kit/text-truncation-cursor-parked-5559.cjs
ARM=before|after SB_PORT=6259
Table cells 3→0, headers 4/8 unmoved probe-kit/table-truncate-tooltip-5559.cjs
ARM=before|after STORY core-table--overflow-behavior
frames + sensor receipts probe-kit/text-truncation-frames-5559.cjs
ARM=before|after SB_PORT=6259
negative control on the new test pnpm exec vitest run packages/core/src/Text/Text.test.tsx
--project ui, with before.diff applied → 1 failed | 31 passed
The four composition seams need a story the repo does not ship; scaffold banked at probe-kit/ZzReview5559.stories.tsx.scaffold (bare Text, bare Heading, inside a consumer Tooltip, with a link descendant). The three probes are new and are banked in probe-kit/.
TIME total 24m
setup 4m worktree + fast-install (11s) + one @astryxdesign/build + storybook dev (warm main reused: no — a fix on an existing PR branch measures both arms in that branch's own worktree, which is the two-worktree rule, not a third)
reading 7m version file, brief, critic, loop mechanics, presentation, harness, rulings, private controls, the diff, and Text/Heading/Tooltip/useTruncation/BaseTable source
measuring 8m 11 probe runs across 4 probes × 2 arms; 2 re-measures
writing 3m draft + critic pass + rewrite
waste 2m the first probe used p.astryx-text — Text renders a <span>, so the ready predicate could never be true; and the first hover pass hovered before the lazy Tooltip had mounted, which read as "0 tooltips after the change" and had to be re-run isolated. The second of those was nearly a false block, and it turned into DISPOSITION 1.
- Whether Chrome actually paints a native
titletooltip for a pointer that was already resting on the element before the attribute appeared.screencaptureon this host returnscould not create image from display, so the native tooltip was never photographed in either arm; everytitleclaim above is attribute-level, and no public sentence claims otherwise. - Safari/WebKit — this Mac runs Chromium only; Playwright's WebKit is killed on launch by the managed-Mac policy.
Nothing was posted. This run was read-only by instruction: no review, no comment,
no push to facebook/astryx, no merge, no auto-merge armed. The verdict above is a
draft awaiting Cindy. The only write this run made anywhere is this record and the
frames branch it embeds.
Pass 1 — failed the draft on ten counts, all procedural, none of which moved the verdict:
- R1e — no sentence saying prior review had been checked. Added: there is none.
- R6d — the radius question "is anyone else already fixing this?" had not been asked. Asked: 205 open PRs, no competitor. #5398 is adjacent Tooltip work with no file overlap, and it will move the mount-window number in DISPOSITION 1.
- R19 — the low-risk class was implied, not stated. Now stated against all four conditions.
-
R1f — the commit type had been checked and not reported. Now reported honest,
with the one thing the
needs:code-reviewlabel claims that the diff does not. - R14e — anchors were verified at head but their line text was not pasted in. Pasted.
-
R14g — "breaking" was asserted without establishing an installed consumer.
Established: core is
0.5.0and notprivate. - R16d — the behavior axis of BREAKING was not "no" and never reached JUDGEMENT. It is now DISPOSITION 0.
-
R16f-repro — numbers were quoted with no way to re-run them. A REPRODUCTIONS
block was added; three probes and a story scaffold are banked in
probe-kit/. - R18g — "not worth benchmarking" is an adjective standing in for a measurement. Replaced with "I did not benchmark it" and the enumeration of what there was to price.
- STEP 0 — the safety pass was run and not written down. Written down.
Pass 2 — one substantive failure and three repairs:
- R2 — the public summary came in at 32 words against a 30-word cap. Cut to 29 by deleting, not compressing.
- The remaining R14e and R16d repairs above landed in this pass.
The first hover probe reported zero tooltips on the PR head and would have read as
"this change removes the tooltip entirely" — a false block, the most expensive error
the loop makes. It was the probe: it hovered before the React.lazy Tooltip had
mounted, so mouseenter had already fired and never fired again. Re-running it with a
wait for the anchor to be wired gave 1, and the same test on the before arm gave 1
as well. What survived is DISPOSITION 1 — a real 161–189 ms window, in both arms, that
belongs to Tooltip and not to this PR.

