Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": 2,
"id": "05a7854d-49d3-40b1-8973-fbbf191883da",
"createdOn": "2026-08-22",
"action": "update",
"payload": {
"id": "#50QRCF",
"detail": "CAUSE FOUND AND FIXED — landed on main 2026-08-22 in PR #2253 (merge 66594dd, fix commit 0cf0493). Full evidence is recorded on #TYZK23; this row is the gate-reliability half of the same defect. Summary: the shifting element is the PWA install card (div.pwa-notice-stack), which can mount during a window in which #main-content is briefly absent from the DOM while Next 16 streams and hydrates the route. Its phone geometry is selected by body:has(#main-content[data-phone-footer-owner=\"hero\"]) …, so a card mounting in that gap paints tall (h=401) and is restyled compact (h=161) when the shell returns — one discrete 0.2230 shift. That also explains observation (1) on this row, the part that looked impossible: the gate flips pass/fail on diffs that cannot influence layout because what varies between runs is TIMING (network speed, and whether beforeinstallprompt fires early enough to land inside the gap), not the diff. Deleting one JSON file changes nothing about the page and everything about which side of that race the run lands on — so head c8b7bcdd passing and head 09ff450c failing was never a contradiction. FIXED in src/components/pwa-lifecycle.tsx: the notice stack is held unmounted until the app shell is present. Reproduced locally at exactly 0.2230 before the fix and 0.000 after, using a synthetic beforeinstallprompt at ~120ms plus network throttling. CI on 0cf0493 (run 32531103787): Lighthouse budget SUCCESS. ALSO LANDED, and worth keeping even after this row closes: scripts/run-lighthouse-budget.mjs now prints layout-shift attribution (selector, snippet, score, raw worst item) when grading fails, before the report directory is deleted. This row previously required downloading a CI artifact that this environment cannot reach; that is no longer necessary, and the next occurrence of any layout-shift breach will name its own element. Stop rules honoured: tolerance not widened, baseline not refreshed. NOT CLOSED HERE — close with #TYZK23 and #KFRC3H once further Lighthouse budget runs confirm.",
"source": "CI run 32531103787 on head 0cf0493; local reproduction 2026-08-22; merged in PR #2253; full trace on #TYZK23",
"baseRowFingerprint": "4d77b7b9d2e1af63d40db9b661c4b25c390e3b65fd30df9e3355733d40d0c892"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": 2,
"id": "50eff4b6-19e0-4992-9596-95501f63226b",
"createdOn": "2026-08-22",
"action": "update",
"payload": {
"id": "#KFRC3H",
"detail": "THE MECHANISM IS NOT THE SELECTOR SHAPE — resolved 2026-08-22, landed on main in PR #2253 (merge 66594dd, fix commit 0cf0493). This row predicted \"if PR #2234 lands and CI is STILL bistable afterward, a fourth mechanism remains\". A fourth mechanism did remain; it has now been traced, reproduced locally and fixed, and it is orthogonal to the double-:has() cleanup. WHAT ACTUALLY FIRES: #main-content briefly stops existing while Next 16 streams the route in and React hydrates it (measured gap roughly t=7.9s to t=9.9s under network throttling). Every phone install-card rule — the one PR #2219 fixed and the eight PR #2234 rewrites alike — is keyed on body:has(#main-content[data-phone-footer-owner=\"hero\"]), so during that gap the OWNERSHIP :has() is false whether or not a second redundant :has() is chained onto it. A card mounting inside the gap paints tall (h=401, bottom gap 92px) and is restyled compact (h=161, bottom gap 8px) when the shell returns: one discrete shift, 0.2230. Removing the redundant second :has() therefore cannot close this, which is exactly consistent with 0.223 surviving BOTH of PR #2219 fixes on run 32477570217 — the observation this row correctly refused to explain away. THE FIX: hold the notice stack unmounted until the app shell is present (src/components/pwa-lifecycle.tsx, useSyncExternalStore over a MutationObserver, with the readyState escape hatch qualified to fire only when the shell has NEVER been seen; an unqualified version was refuted by CI returning the identical 0.2230 and was reverted). CONFIRMED BOTH WAYS: the first successful local reproduction (synthetic beforeinstallprompt at ~120ms plus network throttling) gave exactly 0.2230 before and 0.000 after; CI run 32531103787 on head 0cf0493 reports Lighthouse budget SUCCESS. PR #2234 IS STILL WORTH LANDING — the redundant :has() chains are genuinely racy and its static-contract test guards all nine selectors — but it should be recorded as hardening, not as the cause, and its confirmation criterion of \"N consecutive green CI Lighthouse runs\" can no longer distinguish it from this fix now that both would be on main. Also landed: scripts/run-lighthouse-budget.mjs prints layout-shift attribution on a failing grade, which removes this row blocker of being unable to download the CI artifact. Close with #TYZK23 and #50QRCF once further Lighthouse budget runs confirm.",
"source": "CI run 32531103787 on head 0cf0493; local reproduction 2026-08-22; merged in PR #2253; contrast with run 32477570217",
"baseRowFingerprint": "ee8b9600dbe25e966e967f481f55ee08cd29e0c180a36154eea815b03312be87"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": 2,
"id": "5c3b582c-9dc6-4462-ba5c-563279d9ebc5",
"createdOn": "2026-08-22",
"action": "update",
"payload": {
"id": "#TYZK23",
"detail": "ROOT CAUSE FOUND, REPRODUCED LOCALLY, AND FIXED — landed on main 2026-08-22 in PR #2253 (merge 66594dd), commits bc23075 (diagnostic) and 0cf0493 (fix). This row asked for the shifting node from a run where the shift actually fired; that evidence now exists. (1) ATTRIBUTION. The Lighthouse artifact could not be downloaded (Azure Blob egress blocked by this network policy), so the attribution was moved into the runner instead: scripts/run-lighthouse-budget.mjs now parses layout-shifts / layout-shift-elements / cumulative-layout-shift out of each per-cell report and prints selector, snippet, score and the raw worst item BEFORE the report directory is deleted, but only when grading already failed. CI then printed: \"mobile-root cls=0.2230 / 0.2230 body.min-h-full > div.pwa-notice-stack\", boundingRect {top:654, bottom:815, width:396, height:161}, nodeLabel \"Install Clinical KB … Install app / Not now\" — the COMPACT install card. (2) MECHANISM. #main-content briefly stops existing while Next 16 streams the route in and React hydrates it. The phone install-card geometry is chosen by body:has(#main-content[data-phone-footer-owner=\"hero\"]) …, so a card mounting inside that gap is styled by a selector that is false: it paints tall (h=401, bottom gap 92px), then is restyled compact (h=161, bottom gap 8px) when the shell returns. One discrete restyle, which is why the value recurs to three decimals instead of drifting. (3) LOCAL REPRODUCTION — the first one anyone has achieved, and the answer to this row note that it \"reproduces only in CI\". Four earlier attempts failed because beforeinstallprompt never fires in this container. Dispatching it synthetically from an init script at ~120ms WITH network throttling reproduced 0.2230 exactly, with the trace: t=4726ms #main-content present (owner=hero) -> t=7855ms #main-content GONE -> t=9083ms pwa-notice-stack mounts top=330 bottom=731 h=401 -> t=9930ms #main-content returns, stack top=654 bottom=815 h=161 -> t=9963ms SHIFT value=0.2230 div.pwa-notice-stack. So it was never CI-specific runner contention or Chromium 151 behaviour; it needed a slow network plus an early install prompt, which CI has and a fast local container does not. (4) FIX. src/components/pwa-lifecycle.tsx holds the notice stack unmounted until the app shell is present, via useSyncExternalStore over a MutationObserver on documentElement. The readyState===\"complete\" escape hatch releases the gate ONLY while the shell has never been seen (appShellHasEverMounted===false); an earlier version without that qualifier was refuted by CI returning the identical 0.2230, because load fires ~4s and the gap is at ~9s. That refuted commit was reverted rather than left in place with a message claiming a fix. Same local reproduction after the fix: CLS 0.000. CI on 0cf0493 (run 32531103787): Lighthouse budget SUCCESS, pr-required SUCCESS. (5) STOP RULES HONOURED: the cls tolerance was not widened, the baseline was not refreshed, and nothing was attributed without a run where the shift fired. NOT CLOSED HERE: one green CI run on a bistable metric is weak on its own — the deterministic local before/after is the stronger half. Close this row together with #50QRCF and #KFRC3H after the next Lighthouse budget runs on main-scoped PRs come back green.",
Comment on lines +7 to +8

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refresh the summaries when recording the root cause

When these requests are applied by issues:reconcile, this payload updates only the detail/source, so #TYZK23 retains the summary claiming the defect “reproduces only in CI” even though this new detail proves a local reproduction and says it was never CI-specific. The sibling 50eff4b6-19e0-4992-9596-95501f63226b.json similarly retains “PR #2234 ... unconfirmed pending CI” and says that PR is “still worth landing,” although commit 9b10e59 for #2234 is already an ancestor of this commit’s parent. Because /issues surfaces these summaries for triage, the reconciled ledger will advertise obsolete work; include corrected summary fields and describe #2234 as already landed.

AGENTS.md reference: AGENTS.md:L1130-L1136

Useful? React with 👍 / 👎.

"source": "CI run 32531103787 (Lighthouse budget success) on head 0cf0493; CI attribution output on the failing head; local reproduction 2026-08-22 in the Claude web container; merged in PR #2253",
"baseRowFingerprint": "d619c359340a77f0e6b3594c5976b5543233340c71db0add7b2fafefd74b4b7a"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"version": 2,
"id": "79122ddb-a365-495b-a591-b21fa621aecb",
"createdOn": "2026-08-22",
"action": "add",
"payload": {
"pri": "P3",
"type": "rec",
"summary": "Nothing stops a future :has(#main-content…) rule inheriting the app-shell hydration gap that caused the mobile-/ CLS flake",
"detail": "FILED 2026-08-22 from the fix for #TYZK23 / #50QRCF / #KFRC3H (PR #2253, commit 0cf0493). Established by measurement, not inference: on a throttled phone load, #main-content is absent from the DOM for roughly two seconds while Next 16 streams the route in and React hydrates it (trace: present at t=4726ms, GONE at t=7855ms, back at t=9930ms). Any CSS keyed on body:has(#main-content…) therefore evaluates FALSE for a window during load, and any element mounting inside that window paints with the wrong geometry and is restyled when the shell returns. That is exactly the 0.2230 layout shift three ledger rows chased for a week across four candidate mechanisms. NO LIVE DEFECT TODAY: all eleven current consumers are the PWA install card (src/app/globals.css lines 3018-3047), and 0cf0493 defends that one consumer by holding the notice stack unmounted until the shell is present. What is missing is anything that stops the next one. There is no lint rule, no contract test, and no line in docs/search-chrome-behaviour.md warning that a shell-presence :has() is only sound for an element that cannot mount during hydration — so a twelfth consumer added anywhere would silently reintroduce a bistable CLS that took three ledger rows and several days to attribute. OPTIONS, cheapest first: (a) a paragraph in docs/search-chrome-behaviour.md beside the existing phone-chrome reserve rules, stating the gap and the condition; (b) a static contract test asserting every :has(#main-content…) rule targets a descendant of .pwa-notice-stack, which fails loudly the moment a consumer is added elsewhere and costs nothing to run; (c) a lint rule, almost certainly not worth it for eleven selectors. RECOMMEND (a) plus (b). STOP RULE: do not respond by deleting the :has() selectors. They are the correct way to express phone-composer ownership, and the gap is a property of streaming hydration, not a selector bug.",
"source": "commit 0cf0493 and the local reproduction trace, 2026-08-22; src/app/globals.css lines 3018-3047; related rows #TYZK23, #50QRCF, #KFRC3H",
"issueUlid": "01M0KYT2B62TAQDC83BFQSR2EX"
}
}
Loading