Composable / Filtered Candidate Locators

anchor_locator resolves a single anchor→target relation and grid_locator addresses grid cells; neither supports composable refinement of a candidate set — .within(panel).filter(has_text="Delete").nth(2) — the Selenium-4 / Playwright chained-and-filtered locator idiom. Today refining means re-querying a backend. This is a pure post-filter over boxes from any source (template match, OCR, the a11y tree, Fuse & Order On-Screen Element Boxes).

A Candidates wraps a list of {x, y, width, height, …} boxes; every method returns a new Candidates so chains are side-effect-free and fully unit-testable. Pure-stdlib, imports no PySide6.

Headless API

from je_auto_control import from_boxes

target = (from_boxes(boxes)                    # boxes from any locator
          .within((0, 0, 1920, 120))           # only the toolbar
          .filter(has_text="Delete")           # only delete buttons
          .sort_reading()                       # left-to-right
          .nth(1))                              # the second one
if target.center():
    click(*target.center())

within(region) keeps boxes whose centre is inside the rectangle; filter keeps boxes matching every supplied criterion (has_text substring, near (x, y, max_dist) proximity, min_area / max_area, or an arbitrary predicate); sort_reading orders them; nth / first / last select; resolve() returns the surviving list and center() the first box’s centre. Chains never mutate the original set.

Executor command

AC_locate_chain applies a JSON list of ops to a boxes array in order — {op:"within",region:[…]}, {op:"filter",has_text:…}, {op:"reading"}, {op:"nth",index:…}, {op:"first"}, {op:"last"} — returning {count, boxes, center}. It is exposed as the MCP tool ac_locate_chain and as a Script Builder command under Image.