Pre-Match Settle Gating + Match Persistence

Matching mid-animation is a top flakiness source: the spinner is still spinning, a transition is half-done, and the matcher fires on a transient. smart_waits.wait_until_screen_stable gates a live polling loop and returns a boolean — it cannot score stability on an injectable sequence of frames, nor tell you whether a match held steady across them. match_stability adds both, on injected frames so it is unit-testable: region_stability scores how settled a frame sequence is (consecutive-frame SSIM), and match_persistence checks that a template’s hit holds at the same place across the frames (low jitter), not just in one lucky frame.

It reuses ssim.ssim_compare, visual_match.match_template and grounding_consensus.consensus_point — no new matching code. The frames are injectable (ndarray / path / PIL). Imports no PySide6.

Headless API

from je_auto_control import region_stability, match_persistence

frames = [grab(), grab(), grab()]   # a few snapshots of the same region
if region_stability(frames, settle_threshold=0.99)["stable"]:
    do_the_match()

result = match_persistence("button.png", frames, min_score=0.8, agree_px=8)
if result["persisted"]:
    print("steady hit, jitter", result["jitter"])

region_stability returns {stable, mean_ssim, min_ssim}stable when the lowest consecutive-frame SSIM clears settle_threshold. match_persistence returns {persisted, n_hits, jitter}persisted when the template is found in every frame and the hit centres agree within agree_px (jitter is their spread; 0 = rock steady).

Executor commands

AC_region_stability (frames / settle_threshold{stable, mean_ssim, min_ssim}) and AC_match_persistence (template / frames / min_score / agree_px{persisted, n_hits, jitter}). They are exposed as the MCP tools ac_region_stability / ac_match_persistence (read-only) and as the Script Builder commands Region Stability (frames) / Match Persistence (frames) under Image.