Skip to content

fix(ext): immediate SPA nav handling + stop the detection canary over-firing - #11

Merged
astraedus merged 1 commit into
mainfrom
extension-polish
Jul 26, 2026
Merged

fix(ext): immediate SPA nav handling + stop the detection canary over-firing#11
astraedus merged 1 commit into
mainfrom
extension-polish

Conversation

@astraedus

Copy link
Copy Markdown
Owner

Two non-blocking minors from the final Phase 4 QA pass. Small round on top of the merged 0ca3ad2.

1. Cold first hop held the previous verdict for ~2s

The root cause turned out to be upstream of the settle logic: yt-navigate-finish was routed through the 250ms debounce, and the first refresh after a full page load is the one that notices the URL changed and starts the settle machinery. So the post-nav mutation storm delayed even that. Warm hops only looked fine because the previous navigation's settle timers were still firing.

Navigation is a discrete, known-important event, it's now handled immediately, with the debounced pass still following for DOM settling.

The tradeoff you asked me to weigh

Forcing gray during SETTLING does mean an allowed → allowed hop between two videos by the same channel sits in grayscale for a beat: the byline never changes there, so nothing can confirm freshness and only the backstop ends the wait.

I took that deliberately. Grayscale is cosmetic and self-correcting; holding colour on a video that might be from a channel the user is avoiding is the product failing at its one job. To bound the annoyance without touching the punitive side, colour now has its own shorter backstop (SETTLE_COLOR_MS 2.5s) while the interstitial keeps the full 6s.

The reverse-direction protection is unchanged, the gate stays suppressed throughout, so no regression there.

2. The degraded-detection canary was crying wolf

It counted in-feed ads and Shorts lockups as unidentifiable, so it announced "YouTube changed its DOM" on every ordinary home feed (~15 cards). Those card types have no channel by nature, so their absence proves nothing, and a warning that fires when nothing is wrong is a warning nobody reads, which would have cost us the real signal the canary exists to give.

isChannellessCard now excludes a card that is one, contains one, or sits inside a Shorts shelf. The "contains" case is the one that caught me out: YouTube wraps in-feed ads in an ordinary ytd-rich-item-renderer, so the ad marker is a descendant, not an ancestor, my first attempt still counted the ad.

Verification

Gate Result
npm run lint clean
npm run typecheck clean
npm test 598 passing, 21 files (+6)
npm run build loadable
npm run e2e 37/37 passing

Tests are outcome-phrased. The canary fixture is a realistic mixed feed, ordinary videos, an in-feed ad, a Shorts shelf, and exactly one genuinely unreadable organic card: it counts one, stays silent when that organic card is removed, and leaves every unidentifiable card visible. The colour/verdict asymmetry is asserted directly (colour confirms mid-window while the verdict is still waiting).

Still live-only: the cold-hop timing itself. Fixtures have no mutation storm, so nothing here reproduces the starvation that caused it, over to QA's live re-verify.

Draft on purpose, the orchestrator QA-gates the merge.

…anary crying wolf

Two non-blocking minors from the final Phase 4 QA pass.

1. COLD FIRST HOP held the previous video's verdict (colour and all) for ~2s.
   Root cause was upstream of the settle logic: yt-navigate-finish went through
   the 250ms debounce, and the first refresh after a full load is the one that
   NOTICES the url changed and starts the settle machinery, so the post-nav
   mutation storm delayed even that. Warm hops looked fine only because the
   previous navigation's settle timers were still firing. Navigation is a
   discrete, known-important event and is now handled immediately, with the
   debounced pass still following for the DOM settling after it.

   The tradeoff QA asked me to weigh: forcing gray during SETTLING DOES mean an
   allowed -> allowed hop between two videos BY THE SAME CHANNEL sits in
   grayscale for a beat, because the byline never changes there so nothing can
   confirm freshness and only the backstop ends the wait. Taken deliberately,
   grayscale is cosmetic and self-corrects, whereas holding colour on a video
   that might be from a channel the user is avoiding is the product failing at
   its one job. To bound the annoyance without touching the punitive side,
   colour now has its own shorter backstop (SETTLE_COLOR_MS 2.5s) while the
   interstitial keeps the full 6s. The reverse-direction protection is unchanged:
   the gate stays suppressed throughout.

2. THE DEGRADED-DETECTION CANARY over-fired. It counted in-feed ads and Shorts
   lockups as unidentifiable, so it announced "YouTube changed its DOM" on every
   ordinary home feed (~15 cards). Those card types have no channel BY NATURE, so
   their absence proves nothing, and a warning that fires when nothing is wrong
   is a warning nobody reads, which would have cost us the real signal the canary
   exists to give. isChannellessCard excludes a card that IS one, CONTAINS one
   (YouTube wraps in-feed ads in an ordinary ytd-rich-item-renderer, so the marker
   is a DESCENDANT, the case that made my first attempt still count the ad), or
   sits INSIDE a Shorts shelf.

Tests are outcome-phrased: a mixed feed with an ad, a Shorts shelf and exactly
one genuinely unreadable organic card counts ONE, stays silent when that organic
card is removed, and leaves every unidentifiable card visible. The colour/verdict
asymmetry is asserted directly (colour confirms mid-window while the verdict is
still waiting). The cold-hop timing itself remains live-only, fixtures have no
mutation storm.

Gate: eslint clean, tsc clean, 598 unit tests (21 files, +6), 37/37 e2e.
@astraedus
astraedus marked this pull request as ready for review July 26, 2026 08:52
@astraedus
astraedus merged commit 95153ce into main Jul 26, 2026
2 checks passed
@astraedus
astraedus deleted the extension-polish branch July 26, 2026 08:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant