Skip to content

fix(selectors): de-blind the diagnostics, and fix the two selectors hiding behind them - #735

Merged
ffroliva merged 3 commits into
developfrom
fix/730-carrier-sweep
Sep 7, 2026
Merged

ffroliva merged 3 commits into
developfrom
fix/730-carrier-sweep

Conversation

@ffroliva

@ffroliva ffroliva commented Sep 7, 2026

Copy link
Copy Markdown
Owner

Summary

Scoped down from #730's "sweep 24 constants" after /gflow:predict returned CAUTION 7.3/10 from three personas — unanimous: right change, wrong blast radius. Four changes ship, every one backed by a measurement.

Refs #730 (the full sweep stays open; see "Not touched" below)

The one that matters: our diagnostics were blind

ui_automation.py   _gsQuery         = 'i.google-symbols, span.google-symbols'
diagnostics.py     STRUCTURAL_DOM_JS  querySelectorAll('i.google-symbols')

On flow.google.com, where every Material Symbols ligature rides a <mat-icon>, both returned zero.

The instrument we reach for to diagnose selector drift was blind to the host the drift lives on. That is why #727 and #731 stayed invisible for weeks, and why an incident bundle from a migrated user would have reported no ligatures at all. Both queries are now class-only.

Two selectors were broken behind it

Constant State
SUBMIT_BUTTON_SELECTORS Working by luck. Entries 0–1 matched nothing on migrated; the submit only landed because entry 2 matches the <mat-icon>'s text. Live: prompt_submitted via="button:has-text('arrow_forward')" — after paying two misses per submit
IMAGE_MODEL_PICKER_TRIGGER No twin, no fallback → total silent miss. Best-effort, so generation proceeded on whatever model tier the editor opened at

Both now anchor on the google-symbols class, which sits on the labs <i> and the migrated <mat-icon> alike.

The premise is now observed, not asserted

The whole proposal rested on ".google-symbols matches both carriers" — CSS semantics, never checked. tests/api/transports/test_ligature_carrier.py proves it against a real CSS engine with a two-carrier fixture: no account, no network, no credits. If it ever fails, the premise is wrong and this reverts.

It immediately caught a defect in my own fix: matched 3 of 2. SUBMIT_BUTTON_SELECTORS used :text, a substring match, which also accepts arrow_forward_ios — a real Material Symbol. The <i> qualifier had been containing that; dropping it for the class-only carrier made the over-match reachable on the load-bearing submit path. Now :text-is.

So my own "one character each" framing was wrong: dropping a tag qualifier can expose a latent substring over-match, and each site needs its exactness checked too.

Not touched, each for a measured reason

  • The negation sites (button:not(:has(i.google-symbols)), and a has_not filter). Broadening inverts them — they would exclude more buttons, and on a dialog whose buttons carry mat-icons the iconless set collapses to zero → no include click → a 120s hang. They are also already broken the other way on migrated.
  • The 10 mode-detection selectors. They feed a classifier that calls raise_if_migrated every tick, so they never execute on the host this fixes. Zero upside, nonzero risk on labs — which is unmeasured.
  • The add_2 constants. A ligature name drift, not a carrier one, and _CHARACTER_SLOT_ADD_LIGATURE feeds a JS exact-match heuristic where a string swap is not the fix.

Validation

  • pytest -q --cov — 4043 passed, 7 skipped
  • pyright src — 0 errors · ruff check / format --check clean
  • hygiene 1030 files · doc links · website mirror · memory round trip · PII scan
  • New offline browser fixture, 6 tests, no credits

E2E: the changed selectors sit on the submit and model-picker paths, exercised by the character-create e2e. Not re-run here — the previous live run on this branch's parent predates these selector edits, so I am not claiming live coverage for them. The offline two-carrier fixture is what backs this PR; a live run belongs with whoever takes the remaining sweep.

…her half

Phase 0 for #730, $0, denon82 migrated host. Two surfaces, controls recorded
before any count, hard-error only after the dump is written.

CARRIER RESULT - unambiguous. i.google-symbols matched ZERO for every ligature on
both surfaces; mat-icon matched; and .google-symbols matched exactly the same
count as mat-icon every time. The class sits on both carriers, so a class-only
anchor covers labs AND migrated with no extra cascade entry: the fix for the 24
<i>-only constants is one character each, not 24 duplicated entries.

Two selectors confirmed broken-but-masked:
  - SUBMIT_BUTTON_SELECTORS: both <i> entries miss; only has-text('arrow_forward')
    fires, matching the mat-icon's TEXT rather than its tag. Independently seen
    live the same day. It works by luck.
  - IMAGE_MODEL_PICKER_TRIGGER: arrow_drop_down i=0/mat=1, no twin, no fallback.

THE FINDING A CARRIER-ONLY FIX WOULD HAVE SHIPPED PAST: the migrated host renders
`add`, and renders `add_2` NOWHERE (composer add=1/add_2=0, editor add=2/add_2=0).
That is a ligature NAME change, not a carrier change. NEW_PROJECT_SELECTORS,
ADD_MEDIA_BUTTON and _CHARACTER_SLOT_ADD_SELECTOR all anchor on add_2 - and the
last one ALREADY covers both carriers, so #703 swept it and left it broken.
Changing the carrier on those three would have produced a green sweep and three
selectors that still match nothing.

It also corrects #730's own number: 55 counts literals and double-counts cascades
that alternate carriers. 24 constants is the actionable figure.

The probe found the name drift only because it counted whether the ligature was
present AT ALL, alongside the carrier comparison - a control column added so a
zero could not be misread as absence. Added for epistemic hygiene; it is what
turned up the second defect class.

Also recorded: the first run of this spike died at `Could not find 'New project'
CTA`, because it entered via _enter_editor, which clicks NEW_PROJECT_SELECTORS -
one of the constants under test. A probe must not route through its own subject.
Now navigates straight to the project URL, with the reason in the code.

NOT measured, and stated in the doc: labs (no unmoved account here), ligatures
absent from both surfaces are not proven absent from the product, and whether
broadening to .google-symbols changes .first picks where document order matters.
…iding behind them

Scoped down from #730's "sweep 24 constants" after /gflow:predict returned CAUTION
7.3/10 from three personas, unanimous: right change, wrong blast radius. Four
changes ship, every one backed by a measurement.

THE ONE THAT MATTERS. Our incident-bundle DOM dump queried
'i.google-symbols, span.google-symbols'; diagnostics.py's STRUCTURAL_DOM_JS queried
'i.google-symbols'. On flow.google.com, where every ligature rides a <mat-icon>,
BOTH RETURNED ZERO. The instrument we reach for to diagnose selector drift was blind
to the host the drift lives on - which is why #727 and #731 stayed invisible, and why
a bundle from a migrated user would have named no ligatures at all. Found by the
Devil's Advocate persona, not by me; it is worth more than the other 23 constants.

TWO SELECTORS WERE BROKEN BEHIND IT:
  - SUBMIT_BUTTON_SELECTORS was working BY LUCK. Entries 0 and 1 were tag-qualified
    and matched nothing on migrated; the submit only landed because entry 2 matches
    the mat-icon's TEXT. Live: prompt_submitted via="button:has-text('arrow_forward')",
    after paying two misses at ~2s each on every submit.
  - IMAGE_MODEL_PICKER_TRIGGER had no twin and no fallback, so the miss was total and
    silent: best-effort, so generation proceeded on whatever tier the editor opened at.

Both now anchor on the `google-symbols` CLASS, which sits on the labs <i> and the
migrated <mat-icon> alike. That was CSS semantics asserted, never observed - the
whole proposal rested on it - so tests/api/transports/test_ligature_carrier.py now
proves it against a real CSS engine with a two-carrier fixture. No account, no
network, no credits. If it ever fails, the premise is wrong and this must revert.

THE FIXTURE CAUGHT A DEFECT IN MY OWN FIX: `matched 3 of 2`. SUBMIT_BUTTON_SELECTORS
used `:text`, a SUBSTRING match, which also accepts `arrow_forward_ios` - a real
Material Symbol. The <i> qualifier had been containing that; dropping it for the
class-only carrier made the over-match reachable on the load-bearing submit path.
Now `:text-is`. So the "one character each" framing I used was wrong: dropping a tag
qualifier can expose a latent substring over-match, and each site needs its exactness
checked too. The dominated `button:has(i:text(...))` entry is deleted rather than
broadened.

DELIBERATELY NOT TOUCHED, each for a measured reason:
  - The negation sites, `button:not(:has(i.google-symbols))` and the `has_not` filter.
    Broadening INVERTS them: they would exclude more buttons, and on a dialog whose
    buttons carry mat-icons the iconless set collapses to zero -> no include click ->
    a 120s hang. P1 also found they are ALREADY broken the other way on migrated.
  - The 10 mode-detection selectors in drivers/factory.py and mode_control.py. They
    feed a classifier that calls raise_if_migrated every tick, so they never execute
    on the host this fixes. Zero upside, nonzero risk on labs - which is unmeasured.
  - The add_2 constants. That is a ligature NAME drift, not a carrier one, and
    _CHARACTER_SLOT_ADD_LIGATURE feeds a JS exact-match heuristic where a string swap
    is not the fix.

Verified: 4043 passed, pyright 0 errors, ruff clean, hygiene 1030 files, doc links,
website mirror, memory round trip, PII scan.

Refs #730
…n control

The Iron Law: done means verified by a run. The PR previously said the offline
fixture was what backed it and that a live run was owed. Here it is.

E2E: tests/e2e/test_character_create_e2e.py::test_character_create_format_prompt_
clicks_format_button -> 1 passed in 219.19s on denon82. That exercises _click_submit
with the changed SUBMIT_BUTTON_SELECTORS, end to end, on a real generation.

But pytest swallows stderr on a pass, so it cannot show WHICH cascade entry fired -
and "still works" is not the claim; "works via the fixed selector rather than the
accidental text fallback" is. So the sweep spike now counts the shipped constant and
the form it replaced, side by side, on both surfaces:

  composer          submit             shipped=1  pre_730=0
  composer          image_model_picker shipped=0  pre_730=0
  composer          STRUCTURAL_DOM_JS  47 ligatures, 17 distinct
  character_editor  submit             shipped=1  pre_730=0
  character_editor  image_model_picker shipped=1  pre_730=0
  character_editor  STRUCTURAL_DOM_JS  14 ligatures, 13 distinct

pre_730=0 everywhere is the point: the old anchors matched NOTHING on this host, and
the new ones resolve. The diagnostics went from blind to 47 ligatures - that is the
incident-bundle artifact a migrated user would have sent us, previously empty.

AND THE PROBE CAUGHT ITSELF. The first verification run reported zero ligatures of
any kind on the composer, where an earlier run on the same URL saw 8. The page had
not rendered its icons at the fixed 3s mark. I had given the character editor a
control and left the composer without one, so its zeros were unreadable and I nearly
reported them as findings - the exact failure this week has been about, in my own
instrument. One control on one surface certifies only that surface.

Fixed twice over: every surface now carries its own control and hard-errors after
the dump, and the composer waits for `.google-symbols` to attach rather than sleeping
a fixed interval - the thing being counted is the thing to wait for. Same URL now
yields 17 hits.

That also makes image_model_picker=0 on the composer a MEANINGFUL zero rather than
an unreadable one: the control passed, so that button genuinely is not on that
surface, consistent with the migrated composer having no image mode.

Not measured: labs, still - no unmoved account here.
@ffroliva

ffroliva commented Sep 7, 2026

Copy link
Copy Markdown
Owner Author

Live verification — the E2E gap is closed

E2E: tests/e2e/test_character_create_e2e.py::test_character_create_format_prompt_clicks_format_button → 1 passed in 219.19s on denon82. That drives _click_submit with the changed SUBMIT_BUTTON_SELECTORS through a real generation.

But pytest swallows stderr on a pass, so it cannot show which cascade entry fired — and "still works" was never the claim. "Works via the fixed selector rather than the accidental text fallback" is. So the sweep spike now counts the shipped constant against the form it replaced, on both surfaces:

surface anchor shipped pre-#730
composer submit 1 0
composer image model picker 0 0
composer STRUCTURAL_DOM_JS 47 ligatures, 17 distinct would be 0
character editor submit 1 0
character editor image model picker 1 0
character editor STRUCTURAL_DOM_JS 14 ligatures, 13 distinct would be 0

pre_730=0 everywhere is the point. The old anchors matched nothing on this host; the new ones resolve. And the diagnostics went from blind to 47 ligatures — that is the ui.json a migrated user's incident bundle would have carried, previously empty.

The probe caught itself

The first verification run reported zero ligatures of any kind on the composer, where an earlier run on the same URL saw 8. The page had not rendered its icons at the fixed 3s mark.

I had given the character editor a control and left the composer without one — so those zeros were unreadable, and I nearly reported them as findings. That is precisely the failure this week has been about, reproduced in my own instrument. One control on one surface certifies only that surface.

Fixed twice over: every surface now carries its own control and hard-errors after the dump, and the composer waits for .google-symbols to attach rather than sleeping a fixed interval — the thing being counted is the thing to wait for. Same URL now yields 17 hits.

That also promotes image_model_picker=0 on the composer from an unreadable zero to a meaningful one: the control passed, so that button genuinely is not on that surface — consistent with the migrated composer having no image mode.

Still not measured

labs. No unmoved account exists here. The <i class="google-symbols"> half of the class-only claim is proven by the offline two-carrier fixture against a real CSS engine, not by a live labs run. Named, not hidden.

@ffroliva
ffroliva merged commit 2b9bc2f into develop Sep 7, 2026
15 checks passed
@ffroliva
ffroliva deleted the fix/730-carrier-sweep branch September 7, 2026 19:10
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