"""Shared locator compiler helpers.""" from __future__ import annotations from typing import Any, Optional from models.locator import LocatorCandidate def rank_and_select_primary( candidates: list[LocatorCandidate], ) -> tuple[Optional[LocatorCandidate], list[LocatorCandidate]]: """ Pick production primary only if unique and not coordinate-based. Fallback list excludes primary. """ ranked = sorted( candidates, key=lambda c: ( 0 if c.production_eligible else 1, 0 if c.unique else 1, 0 if c.uses_coordinates else 0, 1 if c.uses_coordinates else 0, 1 if c.uses_index else 0, -float(c.stability_score or 0.0), ), ) primary: Optional[LocatorCandidate] = None for c in ranked: if c.production_eligible and not c.uses_coordinates: primary = c break fallbacks = [c for c in ranked if c is not primary] return primary, fallbacks def count_matches( rows: list[dict[str, Any]], *, properties: dict[str, Any], scope: Optional[dict[str, Any]] = None, ) -> int: """Count rows matching structured properties (snapshot / live tree).""" props = {k: v for k, v in (properties or {}).items() if v not in (None, "")} if not props: return 0 scope = scope or {} n = 0 for r in rows or []: if not isinstance(r, dict): continue if scope.get("window_class"): # optional soft filter if row carries window info rc = r.get("window_class") or r.get("window_class_name") if rc and rc != scope.get("window_class"): continue ok = True for k, v in props.items(): if k == "control_type": rv = r.get("control_type") or r.get("role") or "" elif k == "role": rv = r.get("role") or r.get("control_type") or "" elif k == "text": rv = r.get("text") or r.get("value") or r.get("name") or "" elif k == "test_id": rv = r.get("test_id") or r.get("automation_id") or "" else: rv = r.get(k) if str(rv or "") != str(v): ok = False break if ok: n += 1 return n