Skip to content

The ligature-carrier sweep is unfinished: 55 single-carrier selectors, and nothing watches them #730

Open

Description

@ffroliva

What

#703 swept three selector constants for the labs/migrated ligature-carrier split (<i class="google-symbols"> vs <mat-icon>) — add_2, arrow_drop_down, accessibility_new. It missed PROMPT_FORMAT_SELECTORS, which became a silent no-op and shipped as #727.

Nothing proves that was the last one. Measured on 2026-09-07 at 7c1d4c6d:

git grep "i\.google-symbols" src/gflow_cli --include=*.py | grep -v "mat-icon" | wc -l
55

55 single-carrier literals across ui_automation.py, ui_automation_video.py, mode_control.py, diagnostics.py and drivers/{agentic,factory}.py. Two consecutive PRs have now fixed one of these by hand, which is the signal that this is a sweep problem, not a selector problem.

Named candidates from the #729 council (all pre-existing, none introduced there):

Constant Location Exposure
IMAGE_MODEL_PICKER_TRIGGER ui_automation.py:104 Sharpest. Sole selector, no mat-icon twin — while its sibling _CHARACTER_MODEL_PICKER_TRIGGER_SELECTORS in the same file carries both and a comment recording that omitting mat-icon logged model-picker trigger not found on the migrated host. Likely masked today because _select_image_model only runs after a panel that warn-returns first — not verified on the migrated host.
ui_automation_video.py:251 same shape same class
OVERLAY_CLOSE_BUTTON_SELECTORS ui_automation.py:490-494 clear / close are <i>-only. Degraded, not dead — the [role=dialog]:has(a[href*=changelog]), button[data-dismiss] and Escape fallbacks remain.
NEW_PROJECT_SELECTORS ui_automation.py:329-331 add_2 tier is <i>-only; falls back to a regex + 14 localised labels.

Why nothing caught it, and why nothing will catch the next one

.github/workflows/selector-probe.yml drives run_probe.py --surface editor against src/gflow_cli/flow_selectors/registry.py. That registry has zero entries for the Format button:

grep -rn "personal_recommendations\|format" src/gflow_cli/flow_selectors/ --include=*.py
(no output)

And e2e_character is opt-in (GFLOW_CLI_E2E_RUN_CHARACTER=1); the nightly canary runs e2e_auth only — 18 of 87 tests. So the drift was invisible from both directions at once: no probe watched the selector, and no nightly test exercised the command.

Suggested shape

  1. Sweep the 55, both carriers — or better, anchor on the shared .google-symbols class, which matches <i> and <mat-icon> alike, since the class was never the discriminator (the tag was).
  2. Prefer custom elements where one exists — <flow-format-prompt-button>, <flow-slot-chip-button>, <flow-generate-icon-button> are component boundaries that survive both a carrier change and a translation.
  3. Register the surfaced anchors in flow_selectors/registry.py so selector-probe.yml watches them nightly. This is the half that makes the fix durable.

Worth a /gflow:predict first — item 1 touches ~55 call sites across 6 files and item 3 changes what the nightly probe asserts.

Evidence

Activity

  1. ffroliva commented on Sep 7, 2026

    @ffroliva
    OwnerAuthor

    Partially fixed in #735 — and this issue''s original framing was wrong

    First, a correction to my own issue text. "55 single-carrier literals" counts literals and double-counts cascades that alternate carriers across entries. The actionable unit is the constant:

    • 6 constants already cover both carriers
    • 24 constants are <i>-only ← the real gap

    Shipped in #735 (verified live, both surfaces)

    shipped pre-fix
    SUBMIT_BUTTON_SELECTORS 1 0
    IMAGE_MODEL_PICKER_TRIGGER 1 0
    ui_automation _gsQuery + diagnostics.STRUCTURAL_DOM_JS 47 / 14 ligatures 0

    The diagnostics one is the headline and it was not in this issue. Our incident-bundle DOM dump queried i.google-symbols, span.google-symbols, so on flow.google.com it returned zero ligatures. The instrument used 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 nothing.

    SUBMIT_BUTTON_SELECTORS was also working by luck: its tag-qualified entries matched nothing and the submit only landed via a text fallback matching the <mat-icon>''s text.

    Also: it moved to :text-is. :text is a substring match that accepts arrow_forward_ios, a real Material Symbol — the <i> qualifier had been containing that, and the class-only form made it reachable. So "one character each" is wrong: dropping a tag qualifier can expose a latent substring over-match, and each site needs its exactness checked too.

    Deliberately still open, with measured reasons

    1. The negation sites — button:not(:has(i.google-symbols)) (ui_automation_video.py:432) and a has_not filter (:2199). Broadening inverts them: they would exclude more buttons, and if a dialog''s buttons carry mat-icons the iconless set collapses to zero → no include click → a 120s hang (REMOTE_PICKER_CLOSE_TIMEOUT_S). They are also already broken the other way on migrated, where i.gs=0 makes them match every dialog button — including the tabs their docstring says they exclude. Needs its own spike with the picker dialog open, counting .google-symbols descendants per dialog button.

    2. The 10 mode-detection selectors (drivers/factory.py, 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 — unmeasured.

    3. The add_2 name drift — a different bug, and arguably worse. 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 one. 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 fix: drive character create on the migrated Flow host #703 swept it and left it broken. It also feeds _CHARACTER_SLOT_ADD_LIGATURE into a JS exact-match heuristic, so the fix is not a string swap. This deserves its own issue.

    4. The nightly probe cannot see any of this. flow_selectors/registry.py''s url_template and run_probe.py''s cookie domain are both labs.google. It never touches the migrated host and covers 2 of the 24 constants — it would have stayed green through this entire drift.

    Evidence: docs/superpowers/spikes/2026-09-07-ligature-carrier-and-name-drift.md, probe scripts/dev/spike_ligature_carrier_sweep.py, offline proof tests/api/transports/test_ligature_carrier.py.

  2. ffroliva commented on Sep 7, 2026

    @ffroliva
    OwnerAuthor

    Correction: the add_2 drift is ONE reachable defect, not three — and it is an error message, not a selector

    My earlier comment on this issue said NEW_PROJECT_SELECTORS, ADD_MEDIA_BUTTON and _CHARACTER_SLOT_ADD_SELECTOR "all anchor on add_2 and stay broken". Two of those three are wrong. Traced properly before editing anything:

    Constant Claimed Actually
    _CHARACTER_SLOT_ADD_SELECTOR broken on migrated Not broken. _click_character_slot_add tries _CHARACTER_BODY_MODE_SELECTOR first, which already covers the migrated flow-slot-chip-button. The add_2 path is the legacy fallback — and the constant''s own docstring already said "the migrated host has no add_2 control at all"
    ADD_MEDIA_BUTTON broken on migrated Unreachable. ui_automation_video.py returns FlowHostMigratedError before any of its ligature selectors run
    NEW_PROJECT_SELECTORS broken on migrated Reached only without --project — and migrated_can_serve refuses without one

    All three are labs-only in practice. The name drift has no live production impact on the migrated host today.

    That is the third count in this issue I have had to revise downward (55 literals → 24 constants → 3 broken → 1 reachable defect). The mistake was the same each time: generalising from a grep without checking reachability.

    What was actually fixed — #739

    The reachable defect is the error message. On the migrated host the sweep produced:

    RuntimeError: Could not find 'New project' CTA on Flow gallery.
    URL: https://flow.google.com/?hl=en
    

    which blames the selector for a control that host does not render, with a remediation the reader cannot act on. Now FlowHostMigratedError, naming the host and --project. Labs still reports genuine CTA absence as drift.

    Why it still mattered

    The add_2 drift is harmless because of guards, not because the selectors are right — and guards are what a port relaxes. My own spike hit that message and learned nothing from it. The measured fact (add, never add_2; composer add=1/add_2=0, editor add=2/add_2=0) now sits beside the constant, with why a mat-icon twin would not help.

    Still open on this issue

    1. The negation sites (ui_automation_video.py:432, :2199) — broadening inverts them; risk of a 120s hang. Needs a spike with the picker dialog open.
    2. The 10 mode-detection selectors — never execute on migrated; unmeasured on labs.
    3. The nightly probe is labs-only — registry.py url_template and run_probe.py cookie domain both point at labs.google, covering 2 of 24 constants. It would have stayed green through all of this. This is arguably the most valuable item left, because it is what makes the next drift self-reporting.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions